PyTorch复现GoogleNet学习笔记

一篇简单的学习笔记,实现五类花分类,这里只介绍复现的一些细节

如果想了解更多有关网络的细节,请去看论文《Going Deeper with Convolutions

简单说明下数据集,下载链接,这里用的数据与AlexNet的那篇是一样的所以不在说明

一、环境准备

可以去看之前的一篇博客,里面写的很详细了,并且推荐了一篇炮哥的环境搭建环境

  • Anaconda3(建议使用)
  • python=3.6/3.7/3.8
  • pycharm (IDE)
  • pytorch=1.11.0 (pip package)
  • torchvision=0.12.0 (pip package)
  • cudatoolkit=11.3

二、模型搭建、训练

1.整体框图

GoogleNet整体框图

两个红色框是表示辅助输出器的位置

inception结构

辅助分类器结构

说明:

GoogleNet作为2014年的ILSVRC比赛的冠军相比VGG,网络参数只有vgg的1/10不到

其创新点:

  • 引入了inception结构
  • 使用1*1的卷积进行降维以及映射处理
  • 添加两个辅助分类器帮助训练
  • 丢弃全连接层,使用平均池化层(大大降低网络的参数)

2.net.py

网络整体结构代码

  1 import torch.nn as nn
2 import torch
3 import torch.nn.functional as F
4
5
6 class GoogLeNet(nn.Module):
7 def __init__(self, num_classes=1000, aux_logits=True, init_weights=False):
8 super(GoogLeNet, self).__init__()
9 self.aux_logits = aux_logits
10
11 self.conv1 = BasicConv2d(3, 64, kernel_size=7, stride=2, padding=3)
12 self.maxpool1 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
13
14 self.conv2 = BasicConv2d(64, 64, kernel_size=1)
15 self.conv3 = BasicConv2d(64, 192, kernel_size=3, padding=1)
16 self.maxpool2 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
17
18 self.inception3a = Inception(192, 64, 96, 128, 16, 32, 32)
19 self.inception3b = Inception(256, 128, 128, 192, 32, 96, 64)
20 self.maxpool3 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
21
22 self.inception4a = Inception(480, 192, 96, 208, 16, 48, 64)
23 self.inception4b = Inception(512, 160, 112, 224, 24, 64, 64)
24 self.inception4c = Inception(512, 128, 128, 256, 24, 64, 64)
25 self.inception4d = Inception(512, 112, 144, 288, 32, 64, 64)
26 self.inception4e = Inception(528, 256, 160, 320, 32, 128, 128)
27 self.maxpool4 = nn.MaxPool2d(3, stride=2, ceil_mode=True)
28
29 self.inception5a = Inception(832, 256, 160, 320, 32, 128, 128)
30 self.inception5b = Inception(832, 384, 192, 384, 48, 128, 128)
31
32 if self.aux_logits:
33 self.aux1 = InceptionAux(512, num_classes)
34 self.aux2 = InceptionAux(528, num_classes)
35
36 self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
37 self.dropout = nn.Dropout(0.4)
38 self.fc = nn.Linear(1024, num_classes)
39 if init_weights:
40 self._initialize_weights()
41
42 def forward(self, x):
43 # N x 3 x 224 x 224
44 x = self.conv1(x)
45 # N x 64 x 112 x 112
46 x = self.maxpool1(x)
47 # N x 64 x 56 x 56
48 x = self.conv2(x)
49 # N x 64 x 56 x 56
50 x = self.conv3(x)
51 # N x 192 x 56 x 56
52 x = self.maxpool2(x)
53
54 # N x 192 x 28 x 28
55 x = self.inception3a(x)
56 # N x 256 x 28 x 28
57 x = self.inception3b(x)
58 # N x 480 x 28 x 28
59 x = self.maxpool3(x)
60 # N x 480 x 14 x 14
61 x = self.inception4a(x)
62 # N x 512 x 14 x 14
63 if self.training and self.aux_logits: # eval model lose this layer
64 aux1 = self.aux1(x)
65
66 x = self.inception4b(x)
67 # N x 512 x 14 x 14
68 x = self.inception4c(x)
69 # N x 512 x 14 x 14
70 x = self.inception4d(x)
71 # N x 528 x 14 x 14
72 if self.training and self.aux_logits: # eval model lose this layer
73 aux2 = self.aux2(x)
74
75 x = self.inception4e(x)
76 # N x 832 x 14 x 14
77 x = self.maxpool4(x)
78 # N x 832 x 7 x 7
79 x = self.inception5a(x)
80 # N x 832 x 7 x 7
81 x = self.inception5b(x)
82 # N x 1024 x 7 x 7
83
84 x = self.avgpool(x)
85 # N x 1024 x 1 x 1
86 x = torch.flatten(x, 1)
87 # N x 1024
88 x = self.dropout(x)
89 x = self.fc(x)
90 # N x 1000 (num_classes)
91 if self.training and self.aux_logits: # eval model lose this layer
92 return x, aux2, aux1
93 return x
94
95 def _initialize_weights(self):
96 for m in self.modules():
97 if isinstance(m, nn.Conv2d):
98 nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
99 if m.bias is not None:
100 nn.init.constant_(m.bias, 0)
101 elif isinstance(m, nn.Linear):
102 nn.init.normal_(m.weight, 0, 0.01)
103 nn.init.constant_(m.bias, 0)
104
105
106 class Inception(nn.Module):
107 def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
108 super(Inception, self).__init__()
109
110 self.branch1 = BasicConv2d(in_channels, ch1x1, kernel_size=1)
111
112 self.branch2 = nn.Sequential(
113 BasicConv2d(in_channels, ch3x3red, kernel_size=1),
114 BasicConv2d(ch3x3red, ch3x3, kernel_size=3, padding=1) # 保证输出大小等于输入大小
115 )
116
117 self.branch3 = nn.Sequential(
118 BasicConv2d(in_channels, ch5x5red, kernel_size=1),
119 # 在官方的实现中,其实是3x3的kernel并不是5x5,这里我也懒得改了,具体可以参考下面的issue
120 # Please see https://github.com/pytorch/vision/issues/906 for details.
121 BasicConv2d(ch5x5red, ch5x5, kernel_size=5, padding=2) # 保证输出大小等于输入大小
122 )
123
124 self.branch4 = nn.Sequential(
125 nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
126 BasicConv2d(in_channels, pool_proj, kernel_size=1)
127 )
128
129 def forward(self, x):
130 branch1 = self.branch1(x)
131 branch2 = self.branch2(x)
132 branch3 = self.branch3(x)
133 branch4 = self.branch4(x)
134
135 outputs = [branch1, branch2, branch3, branch4]
136 return torch.cat(outputs, 1)
137
138
139 class InceptionAux(nn.Module):
140 def __init__(self, in_channels, num_classes):
141 super(InceptionAux, self).__init__()
142 self.averagePool = nn.AvgPool2d(kernel_size=5, stride=3)
143 self.conv = BasicConv2d(in_channels, 128, kernel_size=1) # output[batch, 128, 4, 4]
144
145 self.fc1 = nn.Linear(2048, 1024)
146 self.fc2 = nn.Linear(1024, num_classes)
147
148 def forward(self, x):
149 # aux1: N x 512 x 14 x 14, aux2: N x 528 x 14 x 14
150 x = self.averagePool(x)
151 # aux1: N x 512 x 4 x 4, aux2: N x 528 x 4 x 4
152 x = self.conv(x)
153 # N x 128 x 4 x 4
154 x = torch.flatten(x, 1)
155 x = F.dropout(x, 0.5, training=self.training)
156 # N x 2048
157 x = F.relu(self.fc1(x), inplace=True)
158 x = F.dropout(x, 0.5, training=self.training)
159 # N x 1024
160 x = self.fc2(x)
161 # N x num_classes
162 return x
163
164
165 class BasicConv2d(nn.Module):
166 def __init__(self, in_channels, out_channels, **kwargs):
167 super(BasicConv2d, self).__init__()
168 self.conv = nn.Conv2d(in_channels, out_channels, **kwargs)
169 self.relu = nn.ReLU(inplace=True)
170
171 def forward(self, x):
172 x = self.conv(x)
173 x = self.relu(x)
174 return x
175
176 if __name__=="__main__":
177 x = torch.rand([1, 3, 224, 224])
178 model = GoogLeNet(num_classes=5)
179 y = model(x)
180 print(y)
181
182 # 统计模型参数
183 # sum = 0
184 # for name, param in model.named_parameters():
185 # num = 1
186 # for size in param.shape:
187 # num *= size
188 # sum += num
189 # #print("{:30s} : {}".format(name, param.shape))
190 # print("total param num {}".format(sum))#total param num 10,318,655

搭建模型代码

写完后保存,运行可以检查是否报错

如果需要打印模型参数,将代码注释去掉即可,得到googlenet的参数为10,318,655

3.数据划分

这里与AlexNet用的一样

分好后的数据集

运行下面代码将数据按一定比例,划分为训练集和验证集

数据划分的代码

4.train.py

训练的代码,训练结束后画出训练集和验证集的loss,准确度

  1 import json
2 import torch
3 from torch import nn
4 from torchvision import transforms,datasets,utils
5 from torch import optim
6 from torch.optim import lr_scheduler
7 from tqdm import tqdm#用于画进度条
8 from model import GoogLeNet
9 import matplotlib.pyplot as plt
10 import os
11 import sys
12
13
14
15 def main():
16 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
17 print("using {} device".format(device))
18
19 data_transform = {
20 "train": transforms.Compose([transforms.RandomResizedCrop(224),
21 transforms.RandomHorizontalFlip(),
22 transforms.ToTensor(),
23 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]),
24 "val": transforms.Compose([transforms.Resize((224, 224)),
25 transforms.ToTensor(),
26 transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
27
28 #训练集以及测试集路径
29 ROOT_TRAIN = 'data/train'
30 ROOT_TEST = 'data/val'
31
32 batch_size = 16
33
34 train_dataset = datasets.ImageFolder(root=ROOT_TRAIN,transform=data_transform["train"])
35 train_loader = torch.utils.data.DataLoader(train_dataset,batch_size=batch_size,shuffle=True)
36
37 train_num = len(train_dataset)
38
39 flow_list = train_dataset.class_to_idx#转换维字典,train_dataset里有这个对象
40 # {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
41 cla_dict = dict((val,key) for key,val in flow_list.items())#键值对转换
42 #{0: 'daisy', 1: 'dandelion', 2: 'roses', 3: 'sunflowers', 4: 'tulips'}
43 # write dict into json file
44 json_str = json.dumps(cla_dict, indent=4)
45 with open('class_indices.json', 'w') as json_file:
46 json_file.write(json_str) # 保存json文件(好处,方便转换为其它类型数据)用于预测用
47
48 val_dataset = datasets.ImageFolder(root=ROOT_TEST,transform=data_transform["val"])
49 val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
50
51 val_num = len(val_dataset)
52
53 print("using {} images for training, {} images for validation.".format(train_num,
54 val_num))
55 net = GoogLeNet(num_classes=5,aux_logits=True,init_weights=True)
56
57 #加载预训练模型
58 # weights_path = "save_model/best_model.pth"
59 # assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
60 # missing_keys, unexpected_keys = net.load_state_dict(torch.load(weights_path,),strict=False)
61
62 net.to(device)
63 loss_fc = nn.CrossEntropyLoss()
64 optimizer = optim.Adam(net.parameters(), lr=0.0003)
65 # 学习率每隔10epoch变为原来的0.1
66 lr_s = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.5)
67
68 #定义训练函数
69 def train(dataloader, net, loss_fn, optimizer,i,epoch):
70 net.train()
71 loss, current,n = 0.0, 0.0,0
72 train_bar = tqdm(train_loader, file=sys.stdout)
73 for batch, (x, y) in enumerate(train_bar):
74 # 前向传播
75 image, y = x.to(device), y.to(device)
76 logits,aux_logits1,aux_logits2 = net(image)
77 loss0 = loss_fn(logits,y)
78 loss1 = loss_fn(aux_logits1,y)
79 loss2 = loss_fn(aux_logits2,y)
80 cur_loss = loss0 + loss1*0.3 + loss2*0.3#在论文中辅助分类器权重为0.3
81 _, pred = torch.max(logits, axis=-1)
82 cur_acc = torch.sum(y == pred) / logits.shape[0]
83 # 反向传播
84 optimizer.zero_grad() # 梯度清零
85 cur_loss.backward()
86 optimizer.step()
87 loss += cur_loss
88 current += cur_acc
89 n +=1
90 train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(i + 1, epoch, cur_loss)
91 train_loss = loss / n
92 train_acc = current / n
93
94 print(f"tran_loss:{train_loss}")
95 print(f"tran_acc:{train_acc}")
96 return train_loss, train_acc
97
98 def val(dataloader,net,loss_fn):
99 #验证模式
100 net.eval()
101 loss, current ,n = 0.0, 0.0,0
102 with torch.no_grad():
103 val_bar = tqdm(val_loader, file=sys.stdout)
104 for batch,(x,y) in enumerate(val_bar):
105 #前向传播
106 image,y = x.to(device),y.to(device)
107 output = net(image)
108 cur_loss = loss_fn(output,y)
109 _,pred = torch.max(output,axis=-1)
110 cur_acc = torch.sum(y==pred)/output.shape[0]
111 loss += cur_loss
112 current += cur_acc
113 val_bar.desc = "val epoch[{}/{}] loss:{:.3f}".format(i + 1, epoch, cur_loss)
114 n +=1
115 val_loss = loss / n
116 val_acc = current / n
117 print(f"val_loss:{val_loss}")
118 print(f"val_acc:{val_acc}")
119 return val_loss,val_acc
120
121 # 解决中文显示问题
122 plt.rcParams['font.sans-serif'] = ['SimHei']
123 plt.rcParams['axes.unicode_minus'] = False
124
125 # 画图函数
126 def matplot_loss(train_loss, val_loss):
127 plt.figure()
128 plt.plot(train_loss, label='train_loss') # 画图
129 plt.plot(val_loss, label='val_loss')
130 plt.legend(loc='best') # 图例
131 plt.ylabel('loss', fontsize=12)
132 plt.xlabel('epoch', fontsize=12)
133 plt.title("训练集和验证集loss对比图")
134 plt.savefig('./loss.jpg')
135
136 def matplot_acc(train_acc, val_acc):
137 plt.figure()#声明一个新画布,这样两张图像的结果就不会出现重叠
138 plt.plot(train_acc, label='train_acc') # 画图
139 plt.plot(val_acc, label='val_acc')
140 plt.legend(loc='best') # 图例
141 plt.ylabel('acc', fontsize=12)
142 plt.xlabel('epoch', fontsize=12)
143 plt.title("训练集和验证集acc对比图")
144 plt.savefig('./acc.jpg')
145
146 # 开始训练
147 train_loss_list = []
148 val_loss_list = []
149 train_acc_list = []
150 val_acc_list = []
151
152 epoch = 60
153 max_acc = 0
154
155 for i in range(epoch):
156 lr_s.step()#学习率优化,10epoch变为原来的0.5
157
158 train_loss,train_acc = train(train_loader,net,loss_fc,optimizer,i,epoch)
159
160 val_loss,val_acc = val(val_loader,net,loss_fc)
161
162 train_loss_list.append(train_loss)
163 train_acc_list.append(train_acc)
164 val_acc_list.append(val_acc)
165 val_loss_list.append(val_loss)
166
167 # 保存最好的模型权重
168 if val_acc > max_acc:
169 folder = 'save_model'
170 if not os.path.exists(folder):
171 os.mkdir('save_model')
172 max_acc = val_acc
173 print(f'save best model,第{i + 1}轮')
174 torch.save(net.state_dict(), 'save_model/best_model.pth') # 保存网络权重
175 # 保存最后一轮
176 if i == epoch - 1:
177 torch.save(net.state_dict(), 'save_model/last_model.pth') # 保存
178
179 print("done")
180
181 #画图
182 matplot_loss(train_loss_list,val_loss_list)
183 matplot_acc(train_acc_list,val_acc_list)
184 if __name__=="__main__":
185 os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
186 main()

训练代码

最后一轮的结果,总共60轮,3360张训练图片,364张验证图片,用时2h,batchsize=16

训练结束后可以得到训练集和验证集的loss,acc对比图

简单的评估下:可以看到与之前的AlexNet相比,验证集的准确率好多了。val_acc大概再40轮后就一直在84-85浮动,看val_loss,估计在40轮之后对训练集过拟合,从train_acc可以看出,精确度一直在变高,但验证集精确度一直在85左右

三、模型推理

测试代码,这里用的测试集其实是之前训练时的验证集,本来是要另外创建一个的

这里路径需要自己改到需要推理的图片

 1 import os
2 import json
3 import torch
4 from PIL import Image
5 from torchvision import transforms
6 import matplotlib.pyplot as plt
7 from model import GoogLeNet
8
9 def main():
10 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
11
12 data_transform = transforms.Compose([
13 transforms.Resize((224,224)),
14 transforms.ToTensor(),
15 transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))
16 ])
17 #load image
18 img_path = "data/val/daisy/476856232_7c35952f40_n.jpg"
19 assert os.path.exists(img_path),"file:'{}' dose not exist. ".format(img_path)
20 img = Image.open(img_path)
21 plt.imshow(img)
22
23 #[N, C, H, W]归一化
24 img = data_transform(img)
25 # expand batch dimension
26 img = torch.unsqueeze(img,dim=0)
27
28 # read class_indict
29 json_path = './class_indices.json'
30 assert os.path.exists(json_path), "file: '{}' dose not exist.".format(json_path)
31
32 with open(json_path,"r") as f:
33 class_indict = json.load(f)
34
35 #实例化模型
36 model = GoogLeNet(num_classes=5,aux_logits=False).to(device)
37
38 #加载权重
39 weights_path = "save_model/best_model.pth"
40 assert os.path.exists(weights_path), "file: '{}' dose not exist.".format(weights_path)
41 missing_keys,unexpected_keys = model.load_state_dict(torch.load(weights_path,map_location=device),
42 strict=False)
43 model.eval()
44 with torch.no_grad():
45 #预测
46 output = torch.squeeze(model(img.to(device))).cpu()
47 predict = torch.softmax(output, dim=0)
48 predict_cla = torch.argmax(predict).numpy()
49 #最大概率结果
50 print_res = "class: {} prob: {:.3}".format(class_indict[str(predict_cla)],
51 predict[predict_cla].numpy())
52 #前10个结果
53 plt.title(print_res)
54 for i in range(len(predict)):
55 print("class: {:10} prob: {:.3}".format(class_indict[str(i)],
56 predict[i].numpy()))
57 plt.show()
58 if __name__=="__main__":
59 os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
60 main()

模型推理代码

运行代码后,对模型进行推理,去网上找几张图片,我这里随便从验证集拿了一张

下面是一张雏菊的照片,以及5类花预测的概率显示(右边)

总结

googlenet还是挺可以的

自己敲一下代码,会学到很多不懂的东西

最后,多看,多学,多试,总有一天你会称为大佬!

PyTorch复现GoogleNet学习笔记的更多相关文章

  1. Pytorch线性规划模型 学习笔记(一)

    Pytorch线性规划模型 学习笔记(一) Pytorch视频学习资料参考:<PyTorch深度学习实践>完结合集 Pytorch搭建神经网络的四大部分 1. 准备数据 Prepare d ...

  2. Note | PyTorch官方教程学习笔记

    目录 1. 快速入门PYTORCH 1.1. 什么是PyTorch 1.1.1. 基础概念 1.1.2. 与NumPy之间的桥梁 1.2. Autograd: Automatic Differenti ...

  3. pytorch文档学习笔记(2)

    三.CUDA semantics 二.Broadcasting semantics  广播机制 广播机制要第一项对齐,第一项对齐后(相等)才能广播,或者某个第一项为1. 但如果两个size个数相等,则 ...

  4. Pytorch CNN网络MNIST数字识别 [超详细记录] 学习笔记(三)

    目录 1. 准备数据集 1.1 MNIST数据集获取: 1.2 程序部分 2. 设计网络结构 2.1 网络设计 2.2 程序部分 3. 迭代训练 4. 测试集预测部分 5. 全部代码 1. 准备数据集 ...

  5. 【pytorch】学习笔记(四)-搭建神经网络进行关系拟合

    [pytorch学习笔记]-搭建神经网络进行关系拟合 学习自莫烦python 目标 1.创建一些围绕y=x^2+噪声这个函数的散点 2.用神经网络模型来建立一个可以代表他们关系的线条 建立数据集 im ...

  6. 【pytorch】学习笔记(三)-激励函数

    [pytorch]学习笔记-激励函数 学习自:莫烦python 什么是激励函数 一句话概括 Activation: 就是让神经网络可以描述非线性问题的步骤, 是神经网络变得更强大 1.激活函数是用来加 ...

  7. 【pytorch】学习笔记(二)- Variable

    [pytorch]学习笔记(二)- Variable 学习链接自莫烦python 什么是Variable Variable就好像一个篮子,里面装着鸡蛋(Torch 的 Tensor),里面的鸡蛋数不断 ...

  8. 【PyTorch深度学习】学习笔记之PyTorch与深度学习

    第1章 PyTorch与深度学习 深度学习的应用 接近人类水平的图像分类 接近人类水平的语音识别 机器翻译 自动驾驶汽车 Siri.Google语音和Alexa在最近几年更加准确 日本农民的黄瓜智能分 ...

  9. tensorflow学习笔记——VGGNet

    2014年,牛津大学计算机视觉组(Visual Geometry Group)和 Google DeepMind 公司的研究员一起研发了新的深度卷积神经网络:VGGNet ,并取得了ILSVRC201 ...

  10. Vue学习笔记-2

    前言 本文非vue教程,仅为学习vue过程中的个人理解与笔记,有说的不正确的地方欢迎指正讨论 1.computed计算属性函数中不能使用vm变量 在计算属性的函数中,不能使用Vue构造函数返回的vm变 ...

随机推荐

  1. Fluentd直接传输日志给kafka

    官方文档地址:https://docs.fluentd.org/output/kafka td-agent版本自带包含out_kafka2插件,不用再安装了,可以直接使用. 若是使用的是Fluentd ...

  2. redis监控规则

    其他说明参考host主机监控规则:https://www.cnblogs.com/sanduzxcvbnm/p/13589848.html groups: - name: Redis monitori ...

  3. 4.Gitlab CI 与 Kubernetes 的结合

    参考网址:https://www.qikqiak.com/post/gitlab-ci-k8s-cluster-feature/

  4. scss的使用方法

    引用scss文件--看上一篇的less使用,里面的Koala,一样的原理!!! 方法一: scss: /**定义变量 */$width:404px;$color:green;$font-size:20 ...

  5. 谣言检测()——《Debunking Rumors on Twitter with Tree Transformer》

    论文信息 论文标题:Debunking Rumors on Twitter with Tree Transformer论文作者:Jing Ma.Wei Gao论文来源:2020,COLING论文地址: ...

  6. HDU3001 Travelling (状压DP)

    题目没有起点限制,且每个节点至少访问1次,最多访问2次,所以用三进制数表示节点的状态(选取情况). 因为三进制数的每一位是0或1或2,所以预处理z状态S的第j位的数是有必要的. 边界条件:dp[tri ...

  7. 如何用AR Engine环境Mesh能力实现虚实遮挡

    在AR应用中,用户最不希望看到不真实的穿模现象发生,如虚拟形象部分身体陷入墙壁之中,或者未碰到墙壁却已无法移动,这种不真实的交互十分影响用户体验.那如何才能让避免虚拟物体的穿模问题呢?使用AR Eng ...

  8. 虚拟化_Vmware——敬请期待!

    Esxi 7.0 安装部署完成! vSphere vCenter 安装部署完成!

  9. 齐博x1模型里边钩子的创建与使用

    在模型里边的钩子创建与使用方法跟在控制器里边的钩子创建及使用方法是有所区别的在模型里边创建的钩子,你可以理解为执行一个函数,是无法调用模型里边的类的方法及属性的.比如系统文件\application\ ...

  10. 动词时态=>4.将来时态和过去将来时态构成详解

    将来时态构成详解 使用助动词will构成的将来时态 一般将来时态 与一般过去时态相反(时间上) 如果说 一般过去,我们将其当做一张照片 从这张照片当中,我们无法得知 动作什么时候开始 什么时候结束 只 ...