小白也能行【手撕ResNet代码篇(附代码)】:详解可复现
前言
之前已经给大家详解解析了ResNet的原理,其是在什么背景下产生的,这对我们其实有很重要的意义,只有我们了解当时的研究情况,就不会觉得ResNet的出现会突兀,会给我们带来许多思考,如果没看过的,一定先去看看原理篇:一文带你看透什么是ResNet - carpell - 博客园
这里将带大家手撕ResNet代码,小白也没事,一样也能听懂看懂代码的运行逻辑,逻辑一定是最重要的,只有了解其逻辑才有可能自己来复现代码。我们这里就以花的图像分类为例子带大家来看看ResNet代码复现的细节。其与pytorch的官方代码是差不多的。如果可以的话,大家一定要自己尝试去复现一样,自己写的过程中更能够加深自己的理解。
全部的代码位于:fouen6/image_classification_ResNet: 基于resnet的图片分类(pytorch) 下载下来可直接运行复现(只需要配好pytorch环境就行了)
model
BasicBlock 和Bottleneck
首先我们来看ResNet最经典的残差结构,其有两种形式BasicBlock和Bottleneck,其中BasicBlock用于浅层网络,Bottleneck用于深层网络。具体的细节就不讲述了。我们来看代码现的细节。
首先来看BasicBlock部分的代码,在每一个BasicBlock块中,有两层卷积结构,并且都是3x3的卷积,然后还有我们用来处理梯度消失梯度爆炸的BN层,还有我们用来提取特征增加非线性变换的激活层,所以我们就能够很清晰的知道,每个BasicBlock的构成就是(卷积BN激活)x2,同时在输出时进行相加,保证恒等映射,即增加残差结构(out += identity)。还有个注意的地方,第二个激活层是要在(out += identity)之后的。这就是最主要的了,还有些别的细节,再来看,有个参数是expansion,这是干嘛的呢?就是在残差块中进行通道变换的,图中也能看出,输入与输出的通道都是64,所以在这expansion是为1的。这就不难看出后面的Bottleneck中的expansion参数就是为4了,即通道变成输入的4倍。
class BasicBlock(nn.Module):
expansion = 1
def __init__(self,in_planes,planes,stride=1,downsample=None,norm_layer=nn.BatchNorm2d):
super(BasicBlock,self).__init__()
self.conv1 = conv3x3(in_planes,planes,stride)
self.bn1 = norm_layer(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes,planes)
self.bn2 = norm_layer(planes)
self.downsample = downsample
self.stride = stride
def forward(self,x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
然后再来看Bottleneck部分的代码:组成与逻辑上都是与BasicBlock块相似的,一样的部分我就不说了,主要看不同的地方。首先经过一个1x1的卷积,作用是减少通道数,然后在3x3卷积提取特征,在经过1x1的卷积,增加通道数变回原来的通道。先降维再升维的目的就是降低参数量。所以逻辑上很清楚了,按照刚才所说的搭建整个Bottleneck部分就行了。
class Bottleneck(nn.Module):
expansion = 4
def __init__(self,in_planes,planes,stride=1,downsample=None,norm_layer=nn.BatchNorm2d):
super(Bottleneck,self).__init__()
self.conv1 = conv1x1(in_planes,planes,stride)
self.bn1 = norm_layer(planes)
self.conv2 = conv3x3(planes,planes,stride)
self.bn2 = norm_layer(planes)
self.conv3 = conv1x1(planes,planes * self.expansion)
self.bn3 = norm_layer(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self,x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
有个参数downsample一直没讲,因为两部分都有,在这里着重讲一下。其作用就是下采样,当identity与out两者shape不同时,统一shape的。什么时候会用到这个参数呢?看图虚线的地方就代表发生了downsample。要知道我们是将ResNet模块化搭建的,会有着4个layer层,而每个layer层会有着多个的BasicBlock或者是Bottleneck,但是每个layer层的shape是不同的,是向下递减的,举个例子,比如第一个layer是112,那么后续就会变成56,28,14,逐渐下降的。注意这是shape逐渐下降的,但是通道是每个layer变多的。那么不同layer的层的shape不同,不同层之间如何进行恒等相加呢?所以这里就设置了downsample参数,并且其只在每个layer的第一个BasicBlock或者是Bottleneck使用到的。
ResNet
这部分就比较简单了,对于ResNet,首先经过一个初步的特征编码,7x7的卷积,BN层,激活层,池化层,然后就是四个layer层的搭建,最后就是全局池化和全连接层了。这是整体的架构的搭建,在来看一些细节的处理,对于我们所搭建的网络模型,我们肯定是需要进行参数的初始化的,对于卷积层的参数采用凯明初始化,BN层的参数初始化为权重为1,偏置为0。还有个不同的地方就是zero_init_residual的设置,我们将每个BasicBlock或者是Bottleneck的最后一个BN层的权重初始化为0。因为这样是能够对网络的性能有所提升的。还有make_layer函数的部分,我们上面说了只让每个layer的第一个BasicBlock或者是Bottleneck有downsample参数,那么如何控制呢?就是通过stride不为1来让其有downsample参数。
class ResNet(nn.Module):
def __init__(self,block,layers,num_classes=1000,zero_init_residual=False,norm_layer=nn.BatchNorm2d):
super(ResNet,self).__init__()
self.in_planes = 64
self.conv1 = nn.Conv2d(3,64,kernel_size=7,stride=1,padding=3,bias=False)
self.bn1 = norm_layer(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
self.layer1 = self._make_layer(block,64,layers[0],norm_layer=norm_layer)
self.layer2 = self._make_layer(block,128,layers[1],stride=2,norm_layer=norm_layer)
self.layer3 = self._make_layer(block,256,layers[2],stride=2,norm_layer=norm_layer)
self.layer4 = self._make_layer(block,512,layers[3],stride=2,norm_layer=norm_layer)
self.avgpool = nn.AdaptiveAvgPool2d((1,1))
self.fc = nn.Linear(512*block.expansion,num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight,mode='fan_out',nonlinearity='relu')
if isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight,1)
nn.init.constant_(m.bias,0)
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight,0)
if isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight,0)
def _make_layer(self,block,planes,num_blocks,stride=1,norm_layer=nn.BatchNorm2d):
downsample = None
if stride != 1 or self.in_planes != planes *block.expansion:
downsample = nn.Sequential(
conv1x1(self.in_planes,planes * block.expansion,stride),
norm_layer(planes * block.expansion),
)
layers = []
layers.append(block(self.in_planes,planes,stride,downsample,norm_layer))
self.in_planes = planes * block.expansion
for i in range(1,num_blocks):
layers.append(block(self.in_planes,planes))
return nn.Sequential(*layers)
def forward(self,x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0),-1)
x = self.fc(x)
return x
ResNet18\34\50\101\152
然后不同深度的ResNet网络,就是通过控制其是使用BasicBlock或者是Bottleneck,还有每层具体的数量来控制,所以我们可以搭建多个不同深度的ResNet模型。
def resnet18(pretrained=False,**kwargs):
model = ResNet(BasicBlock,[2,2,2,2],**kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet18']))
return model
def resnet34(pretrained=False,**kwargs):
model = ResNet(BasicBlock,[3,4,6,4],**kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet34']))
return model
def resnet50(pretrained=False,**kwargs):
model = ResNet(BasicBlock,[3,4,6,3],**kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet50']))
return model
def resnet101(pretrained=False,**kwargs):
model = ResNet(BasicBlock,[3,4,23,3],**kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet101']))
return model
def resnet152(pretrained=False,**kwargs):
model = ResNet(BasicBlock,[3,8,36,3],**kwargs)
if pretrained:
model.load_state_dict(model_zoo.load_url(model_urls['resnet152']))
return model
data
文件: split.py
功能: 数据集划分脚本。将原始数据集 flower_photos 划分为 train 和 test 两个数据集,并更改图片size=224x224
数据集下载地址:http://download.tensorflow.org/example_images/flower_photos.tgz
数据集保存路径: 根目录 \ dataset \ flower_photos
首先我们要知道我们如何去处理数据的逻辑,首先我们得知道,我们该以什么样的比例来划分数据集,然后就是我们将其处理到224的大小来便于ResNet的处理提取,其实这一步有没有都行,后续训练数据处理的时候也可以进行。所以就是代码实现我们的逻辑就行了,确定划分比例为train:test=8:2。读取我们的文件夹数据,然后逐步处理图片大小,划分训练集测试集。
import os
import glob
import random
import cv2
import numpy as np
if __name__ == '__main__':
split_rate = 0.2 # 训练集和验证集划分比率
resize_image = 224 # 图片缩放后统一大小
file_path = './flower_photos' # 获取原始数据集路径
# 找到文件中所有文件夹的目录,即类文件夹名
dirs = glob.glob(os.path.join(file_path, '*'))
dirs = [d for d in dirs if os.path.isdir(d)]
print("Totally {} classes: {}".format(len(dirs), dirs)) # 打印花类文件夹名称
for path in dirs:
# 对每个类别进行单独处理
path = path.split('\\')[-1] # -1表示以分隔符/保留后面的一段字符
# 在根目录中创建两个文件夹,train/test
os.makedirs("train\\{}".format(path), exist_ok=True)
os.makedirs("test\\{}".format(path), exist_ok=True)
# 读取原始数据集中path类中对应类型的图片,并添加到files中
files = glob.glob(os.path.join(file_path, path, '*jpg'))
files += glob.glob(os.path.join(file_path, path, '*jpeg'))
files += glob.glob(os.path.join(file_path, path, '*png'))
random.shuffle(files) # 打乱图片顺序
split_boundary = int(len(files) * split_rate) # 训练集和测试集的划分边界
for i, file in enumerate(files):
img = cv2.imread(file)
# 更改原始图片尺寸
old_size = img.shape[:2] # (height, width)
ratio = float(resize_image) / max(old_size) # 通过最长的size计算原始图片缩放比率
# 把原始图片最长的size缩放到resize_pic,短的边等比率缩放,等比例缩放不会改变图片的原始长宽比
new_size = tuple([int(x * ratio) for x in old_size])
im = cv2.resize(img, (new_size[1], new_size[0])) # 更改原始图片的尺寸
new_im = np.zeros((resize_image, resize_image, 3), dtype=np.uint8) # 创建一个resize_pic尺寸的黑色背景
# 把新图片im贴到黑色背景上,并通过'地板除//'设置居中放置
x_start = (resize_image - new_size[1]) // 2
y_start = (resize_image - new_size[0]) // 2
new_im[y_start:y_start + new_size[0], x_start:x_start + new_size[1]] = im
# 打印处理进度
print("Processing file {} of {}: {}".format(i + 1, len(files), file))
# 先划分0.2_rate的测试集,剩下的再划分为0.8_ate的训练集,同时直接更改图片后缀为.jpg
if i < split_boundary:
cv2.imwrite(os.path.join("test\\{}".format(path),
file.split('\\')[-1].split('.')[0] + '.jpg'), new_im)
else:
cv2.imwrite(os.path.join("train\\{}".format(path),
file.split('\\')[-1].split('.')[0] + '.jpg'), new_im)
# 统计划分好的训练集和测试集中.jpg图片的数量
train_files = glob.glob(os.path.join('train', '*', '*.jpg'))
test_files = glob.glob(os.path.join('test', '*', '*.jpg'))
print("Totally {} files for train".format(len(train_files)))
print("Totally {} files for test".format(len(test_files)))
train
怎么写train文件,先确定我们整体的逻辑:首先设备的选择gpu确定,然后数据的处理读入,然后模型的搭建(这里有个细节就是,我们选择加载其预训练模型,但是其网络的结构是不对的,因为我们输出的只有5个类,原来有1000,所以我们先加载预训练模型,然后更换全连接层设置输出类别为我们自己的类别),以及优化器,损失函数的确定,接着就是每个epoch训练的逻辑,读取训练数据,模型预测,损失计算,梯度反传更新网络。训练数据结束后我们评估当前epoch的模型训练成果,进行评价,读取测试数据预测评估判断准确率。保存准确率最高的模型的参数。
以下就是将代码的实现的细节过程,知道了整体逻辑再去看代码,思路就会清晰很多,其实这是train文件的整体的设计思路,基本上都是按照这个逻辑来的,当然其中的细节你可以继续优化如何去做,但是整体上的逻辑就基本上都是这样的。再结合代码仔细看看。
import os
import argparse
import sys
import torch.nn as nn
import torch
import time
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import json
import torch.utils.data as Data
from tqdm import tqdm
from model import *
import torch.optim as optim
def get_argparse():
parser = argparse.ArgumentParser()
parser.add_argument('--epochs',type=int,default=10,help='number of epochs')
parser.add_argument('--batch_size',type=int,default=8,help='batch size')
parser.add_argument('--data_path',type=str,default='./dataset/',help='path to dataset')
parser.add_argument('--model',type=str,default='resnet18',help='model name')
parser.add_argument('--lr',type=float,default=0.001,help='learning rate')
parser.add_argument('--save_dir',type=str,default='./checkpoint/',help='save .pth')
parser.add_argument('--num_classes',type=int,default=5,help='number of classes')
return parser
def train(args):
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('Use device:', device)
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
val_transform = transforms.Compose([
transforms.Resize(256), # 图像缩放
transforms.CenterCrop(224), # 中心裁剪
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
train_dataset = datasets.ImageFolder(root=os.path.join(args.data_path, 'train'), transform=train_transform)
train_num = len(train_dataset)
val_dataset = datasets.ImageFolder(root=os.path.join(args.data_path, 'test'), transform=val_transform)
val_num = len(val_dataset)
flower_list = train_dataset.class_to_idx
class_dict = dict((val,key) for key,val in flower_list.items())
json_str = json.dumps(class_dict,indent=4)
with open('class_indices.json','w') as json_file:
json_file.write(json_str)
num_workers = min([os.cpu_count(), args.batch_size if args.batch_size > 1 else 0, 8])
print("Using batch_size={} dataloader worker every process.".format(num_workers))
train_loader = Data.DataLoader(train_dataset,batch_size=args.batch_size,shuffle=True,num_workers=num_workers)
val_loader = Data.DataLoader(val_dataset,batch_size=args.batch_size,num_workers=num_workers,shuffle=False)
print('Number of training images:{}, Number of validation images:{}'.format(train_num,val_num))
model = get_model(args.model)
num_ftrs = model.fc.in_features # 获取全连接层的输入特征数量
model.fc = torch.nn.Linear(num_ftrs, len(flower_list)) # 修改输出维度为5
model = model.cuda()
loss_function = nn.CrossEntropyLoss()
params = [p for p in model.parameters() if p.requires_grad]
optimizer = optim.Adam(params,args.lr)
batch_num = len(train_loader)
total_time = 0
best_acc = 0
for epoch in range(args.epochs):
start_time = time.perf_counter()
model.train()
train_loss = 0
train_bar = tqdm(train_loader,file=sys.stdout)
for step,data in enumerate(train_bar):
train_images,train_labels = data
train_images = train_images.to(device)
train_labels = train_labels.to(device)
optimizer.zero_grad()
outputs = model(train_images)
loss = loss_function(outputs,train_labels)
loss.backward()
optimizer.step()
train_loss += loss.item()
train_bar.desc = "train eopch[{}/{}] loss: {:.3f}".format(epoch+1,args.epochs,loss)
model.eval()
val_acc = 0
var_bar = tqdm(val_loader,file=sys.stdout)
with torch.no_grad():
for val_data in var_bar:
val_images, val_labels = val_data
val_images = val_images.to(device)
val_labels = val_labels.to(device)
val_y=model(val_images)
pred_y = torch.max(val_y,1)[1]
val_acc += torch.eq(pred_y,val_labels).sum().item()
var_bar.desc = "val eopch[{}/{}]".format(epoch+1,args.epochs)
val_accurate = val_acc / val_num
print("[epoch {:.0f}] train_loss: {:.3f} val_accuracy: {:.3f}".format(epoch+1,train_loss/batch_num,val_accurate))
epoch_time = time.perf_counter()-start_time
print("epoch_time:{}".format(epoch_time))
total_time += epoch_time
print()
if val_accurate > best_acc:
best_acc = val_accurate
torch.save(model.state_dict(),os.path.join(args.save_dir,args.model+'_best.pth'))
m,s = divmod(total_time,60)
h,m = divmod(m,60)
print("total time:{:0f}:{:0f}:{:0f}".format(h,m,s))
print("Finished Training!")
if __name__ == '__main__':
args = get_argparse().parse_args()
train(args)
test
测试代码的就是测试我们的模型预测结果了。其实这里的逻辑实现就跟我们train里面的评价阶段是类似的。但是这里会多出可视化的步骤。具体的代码细节看看下面,你就能理解了。
import argparse
import torch
import torch.nn as nn
from matplotlib import pyplot as plt
from model import *
import torchvision.transforms as transforms
import cv2
import os
import json
def get_argparse():
parser = argparse.ArgumentParser()
parser.add_argument('--data_path',type=str,default='./dataset/show',help='path to dataset')
parser.add_argument('--model',type=str,default='resnet18',help='model name')
parser.add_argument('--checkpoint',type=str,default='./checkpoint/resnet18_best.pth',help='checkpoint path')
parser.add_argument('--num_classes',type=int,default=5,help='number of classes')
return parser
def main():
args = get_argparse().parse_args()
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
data_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]),
])
image_path = [os.path.join(args.data_path,f) for f in os.listdir(args.data_path) if f.endswith('.jpg')]
image_path = image_path[:10]
class_indict = json.load(open("./class_indices.json"))
model = get_model(args.model)
num_ftrs = model.fc.in_features # 获取全连接层的输入特征数量
model.fc = torch.nn.Linear(num_ftrs, args.num_classes)
model.load_state_dict(torch.load(args.checkpoint, map_location=device))
model.to(device)
model.eval()
fig , axes = plt.subplots(2,5,figsize=(15,6))
axes = axes.flatten()
for idx, image_path in enumerate(image_path):
img = cv2.imread(image_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = data_transform(img).unsqueeze(0)
img = img.to(device)
with torch.no_grad():
output = model(img)
predict = torch.softmax(output, dim=1)
pred = torch.argmax(predict,dim=1).cpu().numpy()
class_name = class_indict[str(pred[0])] # 获取类别名称
prob = predict[0, pred[0]].item() # 获取预测概率
print_res = "class: {} prob: {:.3f}".format(class_name, prob)
# 显示图片和标题
mean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1).to(device)
std = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1).to(device)
img_for_display = img.squeeze().mul(std).add(mean).permute(1, 2, 0).cpu().numpy() # 调整通道顺序并移动到 CPU
axes[idx].imshow(img_for_display) # 显示图像
axes[idx].set_title(print_res)
axes[idx].axis('off') # 不显示坐标轴
# 隐藏多余的子图(如果有)
for idx in range(len(image_path), len(axes)):
axes[idx].axis('off')
plt.tight_layout()
plt.show()
if __name__ == '__main__':
main()
代码运行以及测试结果
如果你认真看到这,恭喜你,已经对如何利用ResNet模型去进行图片分类任务有了一个比较详细的认知了,以下就是我的代码的运行以及测试的结果。
完整运行代码地址(下载即可用):fouen6/image_classification_ResNet: 基于resnet的图片分类(pytorch)
代码的整体架构就是
首先运行split.py进行数据集的划分,然后就是运行train.py训练模型网络,最后可以通过test.py测试模型。按这个顺序可完整实现上述功能。如有讲的不够好不对的地方欢迎批评指正。
小白也能行【手撕ResNet代码篇(附代码)】:详解可复现的更多相关文章
- Netty实现高性能IOT服务器(Groza)之手撕MQTT协议篇上
前言 诞生及优势 MQTT由Andy Stanford-Clark(IBM)和Arlen Nipper(Eurotech,现为Cirrus Link)于1999年开发,用于监测穿越沙漠的石油管道.目标 ...
- Promise入门到精通(初级篇)-附代码详细讲解
Promise入门到精通(初级篇)-附代码详细讲解 Promise,中文翻译为承诺,约定,契约,从字面意思来看,这应该是类似某种协议,规定了什么事件发生的条件和触发方法. Pr ...
- mysql基础篇 - SELECT 语句详解
基础篇 - SELECT 语句详解 SELECT语句详解 一.实验简介 SQL 中最常用的 SELECT 语句,用来在表中选取数据,本节实验中将通过一系列的动手操作详细学习 SELEC ...
- Scala IDEA for Eclipse里用maven来创建scala和java项目代码环境(图文详解)
这篇博客 是在Scala IDEA for Eclipse里手动创建scala代码编写环境. Scala IDE for Eclipse的下载.安装和WordCount的初步使用(本地模式和集群模式) ...
- 用maven来创建scala和java项目代码环境(图文详解)(Intellij IDEA(Ultimate版本)、Intellij IDEA(Community版本)和Scala IDEA for Eclipse皆适用)(博主推荐)
不多说,直接上干货! 为什么要写这篇博客? 首先,对于spark项目,强烈建议搭建,用Intellij IDEA(Ultimate版本),如果你还有另所爱好尝试Scala IDEA for Eclip ...
- “全栈2019”Java多线程第十八章:同步代码块双重判断详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- 入木三分学网络第一篇--VRRP协议详解第一篇(转)
因为keepalived使用了VRRP协议,所有有必要熟悉一下. 虚拟路由冗余协议(Virtual Router Redundancy Protocol,简称VRRP)是解决局域网中配置静态网关时,静 ...
- iOS开发——控制器OC篇&UINavigationController&UITabBarController详解
UINavigationController&UITabBarController详解 一:UINavigationController 控制器的属性: UINavigationControl ...
- iOS开发——UI篇OC&transform详解
transframe属性详解 1. transform属性 在OC中,通过transform属性可以修改对象的平移.缩放比例和旋转角度 常用的创建transform结构体方法分两大类 (1) 创建“基 ...
- vue.js基础知识篇(6):组件详解
第11章:组件详解 组件是Vue.js最推崇也最强大的功能之一,核心目标是可重用性. 我们把组件代码按照template.style.script的拆分方式,放置到对应的.vue文件中. 1.注册 V ...
随机推荐
- 天翼云加速落地紫金DPU实践应用,让算力供给更高效!
近日,以"智驱创新·芯动未来"为主题的第三届DPU峰会在北京成功举办.会上,天翼云凭借紫金DPU在架构革新.算力释放.场景落地等多方面的成果,荣膺"2023芯星品牌奖&q ...
- Flink白话解析Watermark
一.摘要 如果想使用Flink,Flink的Watermark是很难绕过去的概念.本文帮大家梳理Watermark概念 二.Watermark疑问 1.Flink应用的常见需求是什么 如公司运营一个官 ...
- Luogu P11363 NOIP2024 树的遍历 题解 [ 紫 ] [ 树形 dp ] [ 组合计数 ] [ adhoc ]
树上遍历:CCF 难得一遇的好题! 参考了洛谷的第一篇题解,所以思路会有点相似. 部分分 当 \(k=1\) 时,显然方案总数为 \(\prod_{i=1}^{n}(d_i-1)!\),因为进入一个子 ...
- [CF576E] Painting Edges 题解
模版题的升级了. 使用二分图经典判定方法(一个点拆成两个点 \(x,x+n\),连边 \((x,y)\) 就是连接 \((x,y+n),(x+n,y)\),那么是否是二分图就等价于判断 \(x,x+n ...
- ABB机器人维修时如何零点校准
任何预防性维护计划的主要目的: 1.确保系统的最大利用率. 2.能够减少系统停机时间. 机器人和油漆系统的设计使其能够以最小的维护率在恶劣条件下运行.但是,仍必须定期进行常规检查和预防性维护. 对于工 ...
- 微信小程序之java服务端获取openid
微信小程序越来越热,最近团队写了一个小程序,这篇博客我将讲一下怎么通过java服务端获取到用户的openid. api文档的授权登陆地址: http://developers.weixin.qq.co ...
- Hive - [08] 数据仓库物理模型设计
分区 分区是将表的数据按照某个列的值进行划分和存储的一种方式.通过分区,可以将数据按照特定的维度进行组织,提高查询效率和数据管理的灵活性. 一.分区的优势 提高查询性能:通过分区,可以将数据按照特定的 ...
- 大数据之路Week08_day02 (Flume 三个组件Source, channel, sink)
在使用之前,先介绍组件Flume的特点和一些组件 Flume的优势: 1. Flume可以将应用产生的数据存储到任何集中存储器中,比如HDFS,HBase 2. 当收集数据的速度超过将写入数据的时候, ...
- 非局域网远程访问MySQL
使用内网穿透解决,市面上说道最多的是"花生壳" 主要操作见这篇官方说明 但其中提到的什么花生棒(第二.三点)完全不用管,应该算是产品推销. 登录后选"新增内网映射&quo ...
- 【P3】Logisim搭建单周期MIPS-CPU
最近在想,我究竟能从计组课程中学到什么.依葫芦画瓢地搭一个CPU不难,但稍微设想一下从无到有设计指令,构建数据通路控制器,再到优化为多周期.流水线,在权衡中各模块互相调节...整个过程复杂困难曲折到令 ...