动手学深度学习(PyTorch实现)(二)--softmax与分类模型
softmax与分类模型
- 1. 关于softmax
- 1.1 基本概念
- 1.2 交叉熵损失函数
- 1.3 模型训练与预测
- 2. 获取Fashion-MNIST训练集
- 2.1 下载数据集
- 2.2 处理数据集
- 2.3 读取数据
- 3. PyTorch实现softmax分类模型
- 3.1 加载相应模块
- 3.2 获取数据和初始化参数
- 3.3 定义网络模型并初始化参数
- 3.4 定义损失函数和优化函数
- 3.5 训练模型
1. 关于softmax
1.1 基本概念
分类问题
一个简单的图像分类问题,输入图像的高和宽均为2像素,色彩为灰度。
图像中的4像素分别记为x1,x2,x3,x4x_1, x_2, x_3, x_4x1,x2,x3,x4。
假设真实标签为狗、猫或者鸡,这些标签对应的离散值为y1,y2,y3y_1, y_2, y_3y1,y2,y3。
我们通常使用离散的数值来表示类别,例如y1=1,y2=2,y3=3y_1=1, y_2=2, y_3=3y1=1,y2=2,y3=3。权重矢量
o1=x1w11+x2w21+x3w31+x4w41+b1\begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{21} + x_3 w_{31} + x_4 w_{41} + b_1 \end{aligned} o1=x1w11+x2w21+x3w31+x4w41+b1
o2=x1w12+x2w22+x3w32+x4w42+b2\begin{aligned} o_2 &= x_1 w_{12} + x_2 w_{22} + x_3 w_{32} + x_4 w_{42} + b_2 \end{aligned} o2=x1w12+x2w22+x3w32+x4w42+b2
o3=x1w13+x2w23+x3w33+x4w43+b3\begin{aligned} o_3 &= x_1 w_{13} + x_2 w_{23} + x_3 w_{33} + x_4 w_{43} + b_3 \end{aligned} o3=x1w13+x2w23+x3w33+x4w43+b3
- 神经网络图
下图用神经网络图描绘了上面的计算。softmax回归同线性回归一样,也是一个单层神经网络。由于每个输出o1,o2,o3o_1, o_2, o_3o1,o2,o3的计算都要依赖于所有的输入x1,x2,x3,x4x_1, x_2, x_3, x_4x1,x2,x3,x4,softmax回归的输出层也是一个全连接层。
softmax回归是一个单层神经网络\begin{aligned}softmax回归是一个单层神经网络\end{aligned} softmax回归是一个单层神经网络
既然分类问题需要得到离散的预测输出,一个简单的办法是将输出值oio_ioi当作预测类别是iii的置信度,并将值最大的输出所对应的类作为预测输出,即输出 argmaxioi\underset{i}{\arg\max} o_iiargmaxoi。例如,如果o1,o2,o3o_1,o_2,o_3o1,o2,o3分别为0.1,10,0.10.1,10,0.10.1,10,0.1,由于o2o_2o2最大,那么预测类别为2,其代表猫。
- 输出问题
直接使用输出层的输出有两个问题:- 一方面,由于输出层的输出值的范围不确定,我们难以直观上判断这些值的意义。例如,刚才举的例子中的输出值10表示“很置信”图像类别为猫,因为该输出值是其他两类的输出值的100倍。但如果o1=o3=103o_1=o_3=10^3o1=o3=103,那么输出值10却又表示图像类别为猫的概率很低。
- 另一方面,由于真实标签是离散值,这些离散值与不确定范围的输出值之间的误差难以衡量。
softmax运算符(softmax operator)解决了以上两个问题。它通过下式将输出值变换成值为正且和为1的概率分布:
y^1,y^2,y^3=softmax(o1,o2,o3)\hat{y}_1, \hat{y}_2, \hat{y}_3 = \text{softmax}(o_1, o_2, o_3) y^1,y^2,y^3=softmax(o1,o2,o3)
其中
y^1=exp(o1)∑i=13exp(oi),y^2=exp(o2)∑i=13exp(oi),y^3=exp(o3)∑i=13exp(oi).\hat{y}1 = \frac{ \exp(o_1)}{\sum_{i=1}^3 \exp(o_i)},\quad \hat{y}2 = \frac{ \exp(o_2)}{\sum_{i=1}^3 \exp(o_i)},\quad \hat{y}3 = \frac{ \exp(o_3)}{\sum_{i=1}^3 \exp(o_i)}. y^1=∑i=13exp(oi)exp(o1),y^2=∑i=13exp(oi)exp(o2),y^3=∑i=13exp(oi)exp(o3).
容易看出y^1+y^2+y^3=1\hat{y}_1 + \hat{y}_2 + \hat{y}_3 = 1y^1+y^2+y^3=1且0≤y^1,y^2,y^3≤10 \leq \hat{y}_1, \hat{y}_2, \hat{y}_3 \leq 10≤y^1,y^2,y^3≤1,因此y^1,y^2,y^3\hat{y}_1, \hat{y}_2, \hat{y}_3y^1,y^2,y^3是一个合法的概率分布。这时候,如果y^2=0.8\hat{y}_2=0.8y^2=0.8,不管y^1\hat{y}_1y^1和y^3\hat{y}_3y^3的值是多少,我们都知道图像类别为猫的概率是80%。此外,我们注意到
argmaxioi=argmaxiy^i\underset{i}{\arg\max} o_i = \underset{i}{\arg\max} \hat{y}_i iargmaxoi=iargmaxy^i
因此softmax运算不改变预测类别输出。
- 计算效率
- 单样本矢量计算表达式
为了提高计算效率,我们可以将单样本分类通过矢量计算来表达。在上面的图像分类问题中,假设softmax回归的权重和偏差参数分别为
- 单样本矢量计算表达式
W=[w11w12w13w21w22w23w31w32w33w41w42w43],b=[b1b2b3],\boldsymbol{W} = \begin{bmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ w_{31} & w_{32} & w_{33} \\ w_{41} & w_{42} & w_{43} \end{bmatrix},\quad \boldsymbol{b} = \begin{bmatrix} b_1 & b_2 & b_3 \end{bmatrix}, W=⎣⎢⎢⎡w11w21w31w41w12w22w32w42w13w23w33w43⎦⎥⎥⎤,b=[b1b2b3],
设高和宽分别为2个像素的图像样本iii的特征为
x(i)=[x1(i)x2(i)x3(i)x4(i)],\boldsymbol{x}^{(i)} = \begin{bmatrix}x_1^{(i)} & x_2^{(i)} & x_3^{(i)} & x_4^{(i)}\end{bmatrix}, x(i)=[x1(i)x2(i)x3(i)x4(i)],
输出层的输出为
o(i)=[o1(i)o2(i)o3(i)],\boldsymbol{o}^{(i)} = \begin{bmatrix}o_1^{(i)} & o_2^{(i)} & o_3^{(i)}\end{bmatrix}, o(i)=[o1(i)o2(i)o3(i)],
预测为狗、猫或鸡的概率分布为
y^(i)=[y^1(i)y^2(i)y^3(i)].\boldsymbol{\hat{y}}^{(i)} = \begin{bmatrix}\hat{y}_1^{(i)} & \hat{y}_2^{(i)} & \hat{y}_3^{(i)}\end{bmatrix}. y^(i)=[y^1(i)y^2(i)y^3(i)].
softmax回归对样本iii分类的矢量计算表达式为
o(i)=x(i)W+b,y^(i)=softmax(o(i)).\begin{aligned} \boldsymbol{o}^{(i)} &= \boldsymbol{x}^{(i)} \boldsymbol{W} + \boldsymbol{b},\\ \boldsymbol{\hat{y}}^{(i)} &= \text{softmax}(\boldsymbol{o}^{(i)}). \end{aligned} o(i)y^(i)=x(i)W+b,=softmax(o(i)).
- 小批量矢量计算表达式
为了进一步提升计算效率,我们通常对小批量数据做矢量计算。广义上讲,给定一个小批量样本,其批量大小为nnn,输入个数(特征数)为ddd,输出个数(类别数)为qqq。设批量特征为X∈Rn×d\boldsymbol{X} \in \mathbb{R}^{n \times d}X∈Rn×d。假设softmax回归的权重和偏差参数分别为W∈Rd×q\boldsymbol{W} \in \mathbb{R}^{d \times q}W∈Rd×q和b∈R1×q\boldsymbol{b} \in \mathbb{R}^{1 \times q}b∈R1×q。softmax回归的矢量计算表达式为
O=XW+b,Y^=softmax(O),\begin{aligned} \boldsymbol{O} &= \boldsymbol{X} \boldsymbol{W} + \boldsymbol{b},\\ \boldsymbol{\hat{Y}} &= \text{softmax}(\boldsymbol{O}), \end{aligned} OY^=XW+b,=softmax(O),
其中的加法运算使用了广播机制,O,Y^∈Rn×q\boldsymbol{O}, \boldsymbol{\hat{Y}} \in \mathbb{R}^{n \times q}O,Y^∈Rn×q且这两个矩阵的第iii行分别为样本iii的输出o(i)\boldsymbol{o}^{(i)}o(i)和概率分布y^(i)\boldsymbol{\hat{y}}^{(i)}y^(i)。
1.2 交叉熵损失函数
对于样本iii,我们构造向量y(i)∈Rq\boldsymbol{y}^{(i)}\in \mathbb{R}^{q}y(i)∈Rq ,使其第y(i)y^{(i)}y(i)(样本iii类别的离散数值)个元素为1,其余为0。这样我们的训练目标可以设为使预测概率分布y^(i)\boldsymbol{\hat y}^{(i)}y^(i)尽可能接近真实的标签概率分布y(i)\boldsymbol{y}^{(i)}y(i)。
- 平方损失估计
Loss=∣y^(i)−y(i)∣2/2\begin{aligned}Loss = |\boldsymbol{\hat y}^{(i)}-\boldsymbol{y}^{(i)}|^2/2\end{aligned} Loss=∣y^(i)−y(i)∣2/2
然而,想要预测分类结果正确,我们其实并不需要预测概率完全等于标签概率。例如,在图像分类的例子里,如果y(i)=3y^{(i)}=3y(i)=3,那么我们只需要y^3(i)\hat{y}^{(i)}_3y^3(i)比其他两个预测值y^1(i)\hat{y}^{(i)}_1y^1(i)和y^2(i)\hat{y}^{(i)}_2y^2(i)大就行了。即使y^3(i)\hat{y}^{(i)}_3y^3(i)值为0.6,不管其他两个预测值为多少,类别预测均正确。而平方损失则过于严格,例如y^1(i)=y^2(i)=0.2\hat y^{(i)}_1=\hat y^{(i)}_2=0.2y^1(i)=y^2(i)=0.2比y^1(i)=0,y^2(i)=0.4\hat y^{(i)}_1=0, \hat y^{(i)}_2=0.4y^1(i)=0,y^2(i)=0.4的损失要小很多,虽然两者都有同样正确的分类预测结果。
改善上述问题的一个方法是使用更适合衡量两个概率分布差异的测量函数。其中,交叉熵(cross entropy)是一个常用的衡量方法:
H(y(i),y^(i))=−∑j=1qyj(i)logy^j(i),H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ) = -\sum_{j=1}^q y_j^{(i)} \log \hat y_j^{(i)}, H(y(i),y^(i))=−j=1∑qyj(i)logy^j(i),
其中带下标的yj(i)y_j^{(i)}yj(i)是向量y(i)\boldsymbol y^{(i)}y(i)中非0即1的元素,需要注意将它与样本iii类别的离散数值,即不带下标的y(i)y^{(i)}y(i)区分。在上式中,我们知道向量y(i)\boldsymbol y^{(i)}y(i)中只有第y(i)y^{(i)}y(i)个元素y(i)y(i)y^{(i)}{y^{(i)}}y(i)y(i)为1,其余全为0,于是H(y(i),y^(i))=−logy^y(i)(i)H(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}) = -\log \hat y_{y^{(i)}}^{(i)}H(y(i),y^(i))=−logy^y(i)(i)。也就是说,交叉熵只关心对正确类别的预测概率,因为只要其值足够大,就可以确保分类结果正确。当然,遇到一个样本有多个标签时,例如图像里含有不止一个物体时,我们并不能做这一步简化。但即便对于这种情况,交叉熵同样只关心对图像中出现的物体类别的预测概率。
假设训练数据集的样本数为nnn,交叉熵损失函数定义为
ℓ(Θ)=1n∑i=1nH(y(i),y^(i)),\ell(\boldsymbol{\Theta}) = \frac{1}{n} \sum_{i=1}^n H\left(\boldsymbol y^{(i)}, \boldsymbol {\hat y}^{(i)}\right ), ℓ(Θ)=n1i=1∑nH(y(i),y^(i)),
其中Θ\boldsymbol{\Theta}Θ代表模型参数。同样地,如果每个样本只有一个标签,那么交叉熵损失可以简写成ℓ(Θ)=−(1/n)∑i=1nlogy^y(i)(i)\ell(\boldsymbol{\Theta}) = -(1/n) \sum_{i=1}^n \log \hat y_{y^{(i)}}^{(i)}ℓ(Θ)=−(1/n)∑i=1nlogy^y(i)(i)。从另一个角度来看,我们知道最小化ℓ(Θ)\ell(\boldsymbol{\Theta})ℓ(Θ)等价于最大化exp(−nℓ(Θ))=∏i=1ny^y(i)(i)\exp(-n\ell(\boldsymbol{\Theta}))=\prod_{i=1}^n \hat y_{y^{(i)}}^{(i)}exp(−nℓ(Θ))=∏i=1ny^y(i)(i),即最小化交叉熵损失函数等价于最大化训练数据集所有标签类别的联合预测概率。
1.3 模型训练与预测
在训练好softmax回归模型后,给定任一样本特征,就可以预测每个输出类别的概率。通常,我们把预测概率最大的类别作为输出类别。如果它与真实类别(标签)一致,说明这次预测是正确的。在3.6节的实验中,我们将使用准确率(accuracy)来评价模型的表现。它等于正确预测数量与总预测数量之比
2. 获取Fashion-MNIST训练集
在介绍softmax回归的实现前我们先引入一个多类图像分类数据集。它将在后面的章节中被多次使用,以方便我们观察比较算法之间在模型精度和计算效率上的区别。图像分类数据集中最常用的是手写数字识别数据集MNIST。但大部分模型在MNIST上的分类精度都超过了95%。为了更直观地观察算法之间的差异,我们将使用一个图像内容更加复杂的数据集Fashion-MNIST。
2.1 下载数据集
我这里我们会使用torchvision包,它是服务于PyTorch深度学习框架的,主要用来构建计算机视觉模型。torchvision主要由以下几部分构成:
- torchvision.datasets: 一些加载数据的函数及常用的数据集接口;
- torchvision.models: 包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;
- torchvision.transforms: 常用的图片变换,例如裁剪、旋转等;
- torchvision.utils: 其他的一些有用的方法。
首先,导入相关的包:
# import needed package
# 这么写会让图片内嵌到notebook中
%matplotlib inlinefrom IPython import display
import matplotlib.pyplot as plt
# 导入torch相关的包
import torch
import torchvision
import torchvision.transforms as transforms
import time
# 导入位于路径"/home/kesci/input"下的d2l包
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
然后下载数据集:
mnist_train = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='/home/kesci/input/FashionMNIST2065', train=False, download=True, transform=transforms.ToTensor())
其中:
- root(string)– 数据集的根目录,其中存放processed/training.pt和processed/test.pt文件。
- train(bool, 可选)– 如果设置为True,从training.pt创建数据集,否则从test.pt创建。
- download(bool, 可选)– 如果设置为True,从互联网下载数据并放到root文件夹下。如果root目录下已经存在数据,不会再次下载。
- transform(可被调用 , 可选)– 一种函数或变换,输入PIL图片,返回变换之后的数据。如:transforms.RandomCrop。
- target_transform(可被调用 , 可选)– 一种函数或变换,输入目标,进行变换。
2.2 处理数据集
将数据打上标签:
# 本函数已保存在d2lzh包中方便以后使用
def get_fashion_mnist_labels(labels):text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat','sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']return [text_labels[int(i)] for i in labels]
我们可以看一下得到的数据集是什么样的:
def show_fashion_mnist(images, labels):d2l.use_svg_display()# 这里的_表示我们忽略(不使用)的变量_, figs = plt.subplots(1, len(images), figsize=(12, 12))for f, img, lbl in zip(figs, images, labels):f.imshow(img.view((28, 28)).numpy())f.set_title(lbl)f.axes.get_xaxis().set_visible(False)f.axes.get_yaxis().set_visible(False)plt.show()X, y = [], []
for i in range(10):X.append(mnist_train[i][0]) # 将第i个feature加到X中y.append(mnist_train[i][1]) # 将第i个label加到y中
show_fashion_mnist(X, get_fashion_mnist_labels(y))
输出结果为:
2.3 读取数据
# 小批量中的样本数
batch_size = 256
# 读取数据的线程数
num_workers = 4
# 定义训练集数据加载参数
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
# 定义测试集数据加载参数
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
3. PyTorch实现softmax分类模型
3.1 加载相应模块
# 加载各种包或者模块
import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
sys.path.append("/home/kesci/input")
import d2lzh1981 as d2l
3.2 获取数据和初始化参数
# 每次训练的样本的大小
batch_size = 256
# 从本地数据集中加载训练数据和测试数据
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, root='/home/kesci/input/FashionMNIST2065')
3.3 定义网络模型并初始化参数
定义网络模型:
# 每个样本的特征数
num_inputs = 28*28
# 输出十个类别
num_outputs = 10# 定义线性回归模型
class LinearNet(nn.Module):def __init__(self, num_inputs, num_outputs):super(LinearNet, self).__init__()self.linear = nn.Linear(num_inputs, num_outputs)def forward(self, x): # x 的形状: (batch, 1, 28, 28)y = self.linear(x.view(x.shape[0], -1))return y# 定义转化模型,将输入的维度转化为一维数据
class FlattenLayer(nn.Module):def __init__(self):super(FlattenLayer, self).__init__()def forward(self, x): # x 的形状: (batch, *, *, ...)return x.view(x.shape[0], -1)from collections import OrderedDict
net = nn.Sequential(# FlattenLayer(),# LinearNet(num_inputs, num_outputs) OrderedDict([('flatten', FlattenLayer()),('linear', nn.Linear(num_inputs, num_outputs))]) # 或者写成我们自己定义的 LinearNet(num_inputs, num_outputs) 也可以)
初始化参数:
# 初始化权重,正态分布初始化,均值为0,标准值为0.01
init.normal_(net.linear.weight, mean=0, std=0.01)
# 初始化偏差为0
init.constant_(net.linear.bias, val=0)
3.4 定义损失函数和优化函数
定义损失函数:
定义损失函数为交叉熵函数
loss = nn.CrossEntropyLoss() # 下面是他的函数原型
# class torch.nn.CrossEntropyLoss(weight=None, size_average=None, ignore_index=-100, reduce=None, reduction='mean')
定义优化函数:
优化函数选择随机梯度下降
optimizer = torch.optim.SGD(net.parameters(), lr=0.1) # 下面是函数原型
# class torch.optim.SGD(params, lr=, momentum=0, dampening=0, weight_decay=0, nesterov=False)
3.5 训练模型
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
训练结果为:
其中训练的函数原型为:
def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,params=None, lr=None, optimizer=None):for epoch in range(num_epochs):train_l_sum, train_acc_sum, n = 0.0, 0.0, 0for X, y in train_iter:y_hat = net(X)l = loss(y_hat, y).sum()# 梯度清零if optimizer is not None:optimizer.zero_grad()elif params is not None and params[0].grad is not None:for param in params:param.grad.data.zero_()l.backward()if optimizer is None:d2l.sgd(params, lr, batch_size)else:optimizer.step() train_l_sum += l.item()train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()n += y.shape[0]test_acc = evaluate_accuracy(test_iter, net)print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'% (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)
动手学深度学习(PyTorch实现)(二)--softmax与分类模型相关推荐
- 伯禹公益AI《动手学深度学习PyTorch版》Task 07 学习笔记
伯禹公益AI<动手学深度学习PyTorch版>Task 07 学习笔记 Task 07:优化算法进阶:word2vec:词嵌入进阶 微信昵称:WarmIce 优化算法进阶 emmmm,讲实 ...
- 【动手学深度学习PyTorch版】19 网络中的网络 NiN
上一篇请移步[动手学深度学习PyTorch版]18 使用块的网络 VGG_水w的博客-CSDN博客 目录 一.网络中的网络 NiN 1.1 NiN ◼ 全连接层的问题 ◼ 大量的参数会带来很多问题 ◼ ...
- 伯禹公益AI《动手学深度学习PyTorch版》Task 04 学习笔记
伯禹公益AI<动手学深度学习PyTorch版>Task 04 学习笔记 Task 04:机器翻译及相关技术:注意力机制与Seq2seq模型:Transformer 微信昵称:WarmIce ...
- 动手学深度学习Pytorch Task01
深度学习目前以及未来都有良好的发展前景.正值疫情期间,报名参加了动手学深度学习pytorch版的公开课,希望在以后的学习生活中能够灵活运用学到的这些知识. 第一次课主要包含三个部分:线性回归.soft ...
- 伯禹公益AI《动手学深度学习PyTorch版》Task 06 学习笔记
伯禹公益AI<动手学深度学习PyTorch版>Task 06 学习笔记 Task 06:批量归一化和残差网络:凸优化:梯度下降 微信昵称:WarmIce 批量归一化和残差网络 BN和Res ...
- 【动手学深度学习PyTorch版】6 权重衰退
上一篇移步[动手学深度学习PyTorch版]5 模型选择 + 过拟合和欠拟合_水w的博客-CSDN博客 目录 一.权重衰退 1.1 权重衰退 weight decay:处理过拟合的最常见方法(L2_p ...
- 【动手学深度学习PyTorch版】12 卷积层
上一篇移步[动手学深度学习PyTorch版]11 使用GPU_水w的博客-CSDN博客 目录 一.卷积层 1.1从全连接到卷积 ◼ 回顾单隐藏层MLP ◼ Waldo在哪里? ◼ 原则1-平移不变性 ...
- 【动手学深度学习PyTorch版】27 数据增强
上一篇请移步[动手学深度学习PyTorch版]23 深度学习硬件CPU 和 GPU_水w的博客-CSDN博客 目录 一.数据增强 1.1 数据增强(主要是关于图像增强) ◼ CES上的真实的故事 ◼ ...
- 【动手学深度学习PyTorch版】13 卷积层的填充和步幅
上一篇移步[动手学深度学习PyTorch版]12 卷积层_水w的博客-CSDN博客 目录 一.卷积层的填充和步幅 1.1 填充 1.2 步幅 1.3 总结 二.代码实现填充和步幅(使用框架) 一.卷积 ...
最新文章
- UML系列图--用例图
- 【内容摘录自 MDN】变量作用域
- CSS内置的几个标准的伪类和伪对象
- strtok和sscanf
- 2.3 快速搭建你的第一个系统,并进行迭代-深度学习第三课《结构化机器学习项目》-Stanford吴恩达教授
- linux mysql makefile_Linux安装库文件(环境变量和makefile)
- boost::fusion::as_map用法的测试程序
- (原)用pixi.js 实现 方块阵点击后原地自转效果
- SpringBoot 注解 @Configuration @PropertySource @Value
- 这本京东断货王的Python神书,刷爆整个编程圈!| 测评
- 27. 二叉搜索树与双向链表(C++版本)
- SharePoint 通过控制上传下载对文件进行加密解密(二)
- cad怎么画立体图形教学_立体图形怎么画步骤 找CAD图形中心点的方法步骤图
- 短视频去除logo(抖音)
- LRc2022 M1原生支持功能介绍,Lightroom Classic 2022 Mac M1专用 ,解决lr闪退打不开卡死等一系列问题
- 阿里2018笔试题 之 三种颜色排列
- Forth 语言学习
- 35 实战 微额借款用户人品预测
- Microbit Turnipbit 孩子也能做的声光控开关
- Excel2007版的常用功能(7):Excel公式与函数