四、深度学习的计算

文章目录

  • 前言
  • 一、层和块
    • 1.1 自定义块
    • 1.2 顺序块
    • 1.3 在前向传播函数中执行代码
    • 1.4 效率问题
    • 1.5 小结
  • 二、参数管理
    • 2.1 参数访问
      • 2.1.1 目标参数
      • 2.1.2 访问所有参数
      • 2.1.3 从嵌套块中收集参数
    • 2.2 参数初始化
      • 2.2.1 内置初始化
      • 2.2.2 自定义初始化
      • 2.2.3 参数绑定
  • 三、延后初始化
  • 四、自定义层
    • 4.1 不带参数的层
    • 4.2 带参数的层
  • 五、读写文件
    • 5.1 加载和保存张量
    • 5.2 保存和加载模型参数
  • 六、GPU
    • 6.1 计算设备
    • 6.2 张量与GPU
      • 6.2.1 存储在GPU上
      • 6.2.2 复制
    • 6.3 神经网络与GPU


前言

之前我们已经介绍了一些基本的机器学习概念, 并慢慢介绍了功能齐全的深度学习模型。 在上一章中,我们从零开始实现了多层感知机的每个组件, 然后展示了如何利用高级API轻松地实现相同的模型。 为了易于学习,我们调用了深度学习库,但是跳过了它们工作的细节。 在本章中,我们将深入探索深度学习计算的关键组件, 即模型构建、参数访问与初始化、设计自定义层和块、将模型读写到磁盘, 以及利用GPU实现显著的加速。 这些知识将使读者从深度学习“基础用户”变为“高级用户”。 虽然本章不介绍任何新的模型或数据集, 但后面的高级模型章节在很大程度上依赖于本章的知识。

一、层和块

事实证明,研究讨论“比单个层大”但“比整个模型小”的组件更有价值。

为了试下这些复杂的网络,我们引入了神经网络块的概念。块(block)可以描述单个层、由多个层组成的组件或整个模型本身。 使用块进行抽象的一个好处是可以将一些块组合成更大的组件, 这一过程通常是递归的,如图所示。 通过定义代码来按需生成任意复杂度的块, 我们可以通过简洁的代码实现复杂的神经网络。

在这里插入图片描述
从编程的角度来看,块由类表示。它的任何子类都必须定义一个将其输入转换为输出的前向传播函数, 并且必须存储任何必需的参数。

1.1 自定义块

要想直观地了解块是如何工作的,最简单的方法就是自己实现一个。 在实现我们自定义块之前,我们简要总结一下每个块必须提供的基本功能。

  1. 将输入数据作为其前向传播函数的参数。

  2. 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。

  3. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。

  4. 存储和访问前向传播计算所需的参数。

  5. 根据需要初始化模型参数。

下面我们自定义一个块:

import torch
from torch import nn
from torch.nn import functional as Fclass MLP(nn.Module):# 用模型参数声明层。这里,我们声明两个全连接的层def __init__(self):# 调用MLP的父类Module的构造函数来执行必要的初始化。# 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)super().__init__()self.hidden = nn.Linear(20, 256)  # 隐藏层self.out = nn.Linear(256, 10)  # 输出层# 定义模型的前向传播,即如何根据输入X返回所需的模型输出def forward(self, X):# 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。return self.out(F.relu(self.hidden(X)))
#调用:
net = MLP()
net(X)

块的一个主要优点就是它的多功能性。如:我们可以子类化块以创建层、整个模块或具有中等复杂度的各种组件。

1.2 顺序块

Sequential类的使用:

net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))X = torch.rand(2, 20)
net(X)

现在我们来自习琢磨一下pytorch中的Sequential类是如何工作的,回想下Sequential类的功能,我们需要这两个功能:

  1. 一种将块逐个追加到列表中的函数;
  2. 种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。

自定义实现类似Sequential的功能:

class MySequential(nn.Module):def __init__(self, *args):super().__init__()for idx, module in enumerate(args):# 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员# 变量_modules中。_module的类型是OrderedDictself._modules[str(idx)] = moduledef forward(self, X):# OrderedDict保证了按照成员添加的顺序遍历它们for block in self._modules.values():X = block(X)return Xnet = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)

Module类中有一个_modules的字典,它的主要优点是:在模块的参数初始化过程中,系统知道在_modules字典中查找需要初始化参数的子块。

1.3 在前向传播函数中执行代码

Sequential类虽然简单,但是并不是所有的架构都是简单的顺序架构,有时候我们需要在前向传播中加入python的控制流。如:

class FixedHiddenMLP(nn.Module):def __init__(self):super().__init__()# 不计算梯度的随机权重参数。因此其在训练期间保持不变self.rand_weight = torch.rand((20, 20), requires_grad=False)self.linear = nn.Linear(20, 20)def forward(self, X):X = self.linear(X)# 使用创建的常量参数以及relu和mm函数X = F.relu(torch.mm(X, self.rand_weight) + 1)# 复用全连接层。这相当于两个全连接层共享参数X = self.linear(X)# 控制流while X.abs().sum() > 1:X /= 2return X.sum()

在这个FixedHiddenMLP模型中,我们实现了一个隐藏层, 其权重(self.rand_weight)在实例化时被随机初始化,之后为常量。 这个权重不是一个模型参数,因此它永远不会被反向传播更新。 然后,神经网络将这个固定层的输出通过一个全连接层。

注意,在返回输出之前,模型做了一些不寻常的事情: 它运行了一个while循环,在L1范数大于1的条件下, 将输出向量除以2,直到它满足条件为止。 最后,模型返回了X中所有项的和。 注意,此操作可能不会常用于在任何实际任务中, 我们只展示如何将任意代码集成到神经网络计算的流程中。

我们也可以嵌套块:

class NestMLP(nn.Module):def __init__(self):super().__init__()self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),nn.Linear(64, 32), nn.ReLU())self.linear = nn.Linear(32, 16)def forward(self, X):return self.linear(self.net(X))chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)

1.4 效率问题

注:我们在模型中加入python的代码可能会拖慢执行速度,因为GPU上的运行极快,提高Python速度的最好方法是完全避免使用Python。

1.5 小结

  • 一个块可以由许多层组成;一个块可以由许多块组成。

  • 块可以包含代码。

  • 块负责大量的内部处理,包括参数初始化和反向传播。

  • 层和块的顺序连接由Sequential块处理。

二、参数管理

在本部分,我们将介绍一下内容:

  • 访问参数,用于调试、诊断和可视化;
  • 参数初始化;
  • 在不同模型组件间共享参数。

先看下单隐藏层的多层感知机:

import torch
from torch import nnnet = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)

2.1 参数访问

我们可以通过索引来访问模型的任意层,这就像模型是一个列表,列表中每个元素的state_dict属性存放参数。如:

print(net[0])
print(net[0].state_dict())
print(net[1])
print(net[2])
Linear(in_features=4, out_features=8, bias=True)
OrderedDict([('weight', tensor([[ 0.1200,  0.0939,  0.3031,  0.4134],[ 0.3665,  0.2180,  0.0131,  0.4878],[ 0.3440, -0.4701,  0.0074,  0.2202],[-0.2532, -0.4209,  0.0032,  0.4046],[ 0.4745,  0.0568,  0.1719,  0.2846],[-0.1002, -0.3926,  0.0540,  0.2673],[ 0.4484,  0.3800, -0.2103, -0.4893],[-0.1503,  0.4347,  0.1127,  0.3866]])), ('bias', tensor([ 0.1516, -0.3084,  0.0946,  0.0946, -0.4010,  0.3626, -0.4877,  0.3704]))])
ReLU()
Linear(in_features=8, out_features=1, bias=True)

2.1.1 目标参数

注意,每个参数都表示为参数类的一个实例。 要对参数执行任何操作,首先我们需要访问底层的数值。 有几种方法可以做到这一点。有些比较简单,而另一些则比较通用。 下面的代码从第二个全连接层(即第三个神经网络层)提取偏置, 提取后返回的是一个参数类实例,并进一步访问该参数的值。

print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([-0.0291], requires_grad=True)
tensor([-0.0291])

参数是一个符合的对象,它具有值(.data)、梯度(.grad)和额外的信息属性.

net[2].weight.grad == None

2.1.2 访问所有参数

当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦。下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层。

print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))

2.1.3 从嵌套块中收集参数

首先,我们定义一个生成块的函数:

def block1():return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),nn.Linear(8, 4), nn.ReLU())def block2():net = nn.Sequential()for i in range(4):# 在这里嵌套net.add_module(f'block {i}', block1())return netrgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)

我们可以直接打印网络模型,看其结构:

print(rgnet)
Sequential((0): Sequential((block 0): Sequential((0): Linear(in_features=4, out_features=8, bias=True)(1): ReLU()(2): Linear(in_features=8, out_features=4, bias=True)(3): ReLU())(block 1): Sequential((0): Linear(in_features=4, out_features=8, bias=True)(1): ReLU()(2): Linear(in_features=8, out_features=4, bias=True)(3): ReLU())(block 2): Sequential((0): Linear(in_features=4, out_features=8, bias=True)(1): ReLU()(2): Linear(in_features=8, out_features=4, bias=True)(3): ReLU())(block 3): Sequential((0): Linear(in_features=4, out_features=8, bias=True)(1): ReLU()(2): Linear(in_features=8, out_features=4, bias=True)(3): ReLU()))(1): Linear(in_features=4, out_features=1, bias=True)
)

通过索引打印参数:

rgnet[0][1][0].bias.data

2.2 参数初始化

知道了如何访问参数后, 我们需要看下如何正确的初始化参数。

默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。 PyTorch的nn.init模块提供了多种预置初始化方法。

2.2.1 内置初始化

让我们首先调用内置的初始化器。 下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为0。

def init_normal(m):if type(m) == nn.Linear:nn.init.normal_(m.weight, mean=0, std=0.01)nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]

net.apply是PyTorch中的一个方法,用于将一个函数应用到一个网络模型的所有参数上。

我们还可以将所有参数初始化为给定的常数,比如初始化为1。

def init_constant(m):if type(m) == nn.Linear:nn.init.constant_(m.weight, 1)nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]

我们还可以对某些块应用不同的初始化方法。 例如,下面我们使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42。

def init_xavier(m):if type(m) == nn.Linear:nn.init.xavier_uniform_(m.weight)
def init_42(m):if type(m) == nn.Linear:nn.init.constant_(m.weight, 42)net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)

2.2.2 自定义初始化

有时,深度学习框架没有提供我们需要的初始化方法。 在下面的例子中,我们使用以下的分布为任意权重参数w定义初始化方法:
w ∼ { U ( 5 , 10 ) 可能性  1 4 0 可能性  1 2 U ( − 10 , − 5 ) 可能性  1 4 \begin{split}\begin{aligned} w \sim \begin{cases} U(5, 10) & \text{ 可能性 } \frac{1}{4} \\ 0 & \text{ 可能性 } \frac{1}{2} \\ U(-10, -5) & \text{ 可能性 } \frac{1}{4} \end{cases} \end{aligned}\end{split} w U(5,10)0U(10,5) 可能性 41 可能性 21 可能性 41

def my_init(m):if type(m) == nn.Linear:print("Init", *[(name, param.shape)for name, param in m.named_parameters()][0])nn.init.uniform_(m.weight, -10, 10)m.weight.data *= m.weight.data.abs() >= 5net.apply(my_init)
net[0].weight[:2]

我们也可以直接设置参数:

net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]

2.2.3 参数绑定

有时我们希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。

# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),shared, nn.ReLU(),shared, nn.ReLU(),nn.Linear(8, 1))
net(X)
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100  #修改一下一个层的参数
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])

这个例子表明第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。 这里有一个问题:当参数绑定时,梯度会发生什么情况? 答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。

三、延后初始化

延后初始化能让我们先定义整个模型,而不用关心它输入的维度。

import torch
from torch import nn
from d2l import torch as d2l
net = nn.Sequential(nn.LazyLinear(256), nn.ReLU(), nn.LazyLinear(10))

此时,我们看看权重:

net[0].weight

结果是:

<UninitializedParameter>

当我们给定输入时,模型会自定初始化参数:

X = torch.rand(2, 20)
net(X)net[0].weight.shape
torch.Size([256, 20])

四、自定义层

深度学习成功背后的一个因素就是神经网络的灵活性,有时我们为了解决特定的问题需要自定义一个框架中不存在的层。目前我们已知的有Linear层,接下来我们展示如何自定义层。

4.1 不带参数的层

首先,我们构造一个没有任何参数的自定义层。下面的CenteredLayer类要从其输入中减去均值。 要构建它,我们只需继承基础层类并实现前向传播功能。

import torch
import torch.nn.functional as F
from torch import nnclass CenteredLayer(nn.Module):def __init__(self):super().__init__()def forward(self, X):return X - X.mean()

使用:

net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
Y = net(torch.rand(4, 8))
Y.mean()

结果为1个很小很小的数。

4.2 带参数的层

class MyLinear(nn.Module):def __init__(self, in_units, units):super().__init__()self.weight = nn.Parameter(torch.randn(in_units, units))self.bias = nn.Parameter(torch.randn(units,))def forward(self, X):linear = torch.matmul(X, self.weight.data) + self.bias.datareturn F.relu(linear)

接下来我们实例化并访问参数:

linear = MyLinear(5, 3)
linear.weight
Parameter containing:
tensor([[ 1.9094, -0.8244, -1.6846],[ 0.6850,  0.8366, -1.3837],[ 0.0289,  2.0976,  1.3855],[-0.8574, -0.3557, -0.4109],[ 2.2963, -1.3008,  1.2173]], requires_grad=True)

使用:

net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))

五、读写文件

到目前为止,我们讨论了如何处理数据, 以及如何构建、训练和测试深度学习模型。 然而,有时我们希望保存训练的模型, 以备将来在各种环境中使用(比如在部署中进行预测)。 此外,当运行一个耗时较长的训练过程时, 最佳的做法是定期保存中间结果, 以确保在服务器电源被不小心断掉时,我们不会损失几天的计算结果。 因此,现在是时候学习如何加载和存储权重向量和整个模型了。

5.1 加载和保存张量

对于单个张量,我们可以直接调用loadsave函数分别读写它们。 这两个函数都要求我们提供一个名称,save要求将要保存的变量作为输入。

import torch
from torch import nn
from torch.nn import functional as Fx = torch.arange(4)
torch.save(x, 'x-file')
x2 = torch.load('x-file')

我们也可以保存一个张量列表或张量字典,然后再读取回来:

y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')

5.2 保存和加载模型参数

class MLP(nn.Module):def __init__(self):super().__init__()self.hidden = nn.Linear(20, 256)self.output = nn.Linear(256, 10)def forward(self, x):return self.output(F.relu(self.hidden(x)))net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)

接下来,我们将模型的参数存储在一个叫做“mlp.params”的文件中。

torch.save(net.state_dict(), 'mlp.params')

加载:

clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval() #将模型设置为预测模式 不反向传播更新参数

六、GPU

在命令行我们可以输入nvidia-smi查看GPU信息:
在这里插入图片描述

6.1 计算设备

在pytorch中,可以使用如下方法调用:

import torch
from torch import nntorch.device('cpu'), torch.device('cuda'), torch.device('cuda:1')

查询可用gpu数:

torch.cuda.device_count()

6.2 张量与GPU

x = torch.tensor([1, 2, 3])
x.device  #可以查看当前x张量在cpu还是gpu上

需要注意的是,无论何时我们要对多个项进行操作, 它们都必须在同一个设备上。 例如,如果我们对两个张量求和, 我们需要确保两个张量都位于同一个设备上, 否则框架将不知道在哪里存储结果,甚至不知道在哪里执行计算。

6.2.1 存储在GPU上

法一:

X = torch.ones(2, 3, device=torch.device('cuda:0'))  #放到gpu0上

法二:

X = torch.ones(2, 3)
X.to(torch.device('cuda:0'))

法三:

X = torch.ones(2, 3)
X.cuda()

6.2.2 复制

如果我们要计算X + Y,我们需要决定在哪里执行这个操作。 例如,如图所示, 我们可以将X传输到第二个GPU并在那里执行操作。 不要简单地X加上Y,因为这会导致异常, 运行时引擎不知道该怎么做:它在同一设备上找不到数据会导致失败。 由于Y位于第二个GPU上,所以我们需要将X移到那里, 然后才能执行相加运算。
在这里插入图片描述

Z = X.cuda(1)
print(X)
print(Z)

结果:

tensor([[1., 1., 1.],[1., 1., 1.]], device='cuda:0')
tensor([[1., 1., 1.],[1., 1., 1.]], device='cuda:1')

此时如果X+Z,则:
报错:RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:1 and cpu!

6.3 神经网络与GPU

我们也可以把模型放到GPU上:

net = nn.Sequential(nn.Linear(3, 1))
net = net.to(device=torch.device('cuda:0'))

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/6109.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

动态规划详解Python

动态规划 动态规划&#xff08;Dynamic Programming&#xff09;是一种用于解决复杂问题的算法设计方法。它通常用于优化问题&#xff0c;其中问题可以被分解成一系列重叠子问题&#xff0c;通过存储并重复使用已经解决过的子问题的解&#xff0c;可以避免重复计算&#xff0c…

第7章 Scala集合

第7章 Scala集合 7.1 简介 ​ ​ scala.collection.immutable ​ scala.collection.mutable ​ 7.2 数组 ​ 不可变数组 package chapter07object Test01_ImmutableArray {def main(args: Array[String]): Unit {// 1. 创建数组val arr: Array[Int] new Array[Int](10…

【数据结构与算法】4、双向链表(学习 jdk 的 LinkedList 部分源码)

目录 一、双向链表二、node(int index) 根据索引找节点三、clear()四、add(int, E&#xff09;五、remove(int index)六、双向链表和单链表七、双向链表和动态数组八、jdk 官方的 LinkedList 的 clear() 方法 一、双向链表 &#x1f381; 单链表的节点中只有一个 next 指针引用…

STM32面试知识点总结分析

一、STM32F1和F4的区别&#xff1f; 内核不同&#xff1a;F1是Cortex-M3内核&#xff0c;F4是Cortex-M4内核&#xff1b; 主频不同&#xff1a;F1主频72MHz&#xff0c;F4主频168MHz&#xff1b; 浮点运算&#xff1a;F1无浮点运算单位&#xff0c;F4有&#xff1b; 功能性…

高压线路零序电流方向保护程序逻辑原理(四)

2&#xff0e;全相循环程序逻辑框图 全相循环程序逻辑简图如图3&#xff0d;18所示。程序入口首先检测标志位UQDB1&#xff0c;在采样中断服务程序中采用3U。突变量来区分接地故障和TA断线&#xff0c;接地故障时Δ3U。大于定值&#xff0c;置标志位UQDB1&#xff0c;这是11型…

Elasticsearch 安装

下载安装 elasticsearch下载链接 运行&#xff1a;bin\elasticsearch.bat 设置密码&#xff1a;.\bin\elasticsearch-setup-passwords interactive 这边设置密码遇到一个坑 PS G:\elasticsearch-8.8.1> .\bin\elasticsearch-setup-passwords interactiveFailed to authe…

AI实战营第二期 第七节 《语义分割与MMSegmentation》——笔记8

文章目录 摘要主要特性 案例什么是语义分割应用&#xff1a;无人驾驶汽车应用&#xff1a;人像分割应用&#xff1a;智能遥感应用 : 医疗影像分析 三种分割的区别语义分割的基本思路按颜色分割逐像素份分类全卷积网络 Fully Convolutional Network 2015存在问题 基于多层级特征…

Python 基于招聘数据可视化系统

1 简介 Python 基于招聘数据可视化系统&#xff0c;视频效果如下&#xff1a; 基于Python的招聘信息可视化系统&#xff0c;附源码 随着国内的经济不断的快速发展&#xff0c;现在学生的就业压力也在逐年增加&#xff0c;网络上的招聘信息非常的丰富&#xff0c;但是对于学生而…

高等数学下拾遗+与matlab结合

如何学好高等数学 高等数学是数学的一门重要分支&#xff0c;包括微积分、线性代数、常微分方程等内容&#xff0c;它是许多理工科专业的基础课程。以下是一些学好高等数学的建议&#xff1a; 扎实的基础知识&#xff1a;高等数学的内容很多&#xff0c;包括初等数学的一些基…

回归预测 | MATLAB实现PSO-CNN粒子群算法优化卷积神经网络的数据多输入单输出回归预测

回归预测 | MATLAB实现PSO-CNN粒子群算法优化卷积神经网络的数据多输入单输出回归预测 目录 回归预测 | MATLAB实现PSO-CNN粒子群算法优化卷积神经网络的数据多输入单输出回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 回归预测 | MATLAB实现PSO-CNN粒子群算法优…

通过Jenkins实现Unity多平台自动打包以及相关问题解决

简介 通过本文可以了解到如何在windows和mac上部署Jenkins。并且通过Jenkins实现Unity在IOS,安卓和PC等多平台自动打包的功能&#xff0c;并且可以将打包结果通过飞书机器人同步到飞书群内。优化工作流&#xff0c;提高团队的开发效率。文末记录了实际使用Jenkins时遇到的各种问…

探索数字化前沿:数字化产品引领科技创新风潮

随着数字化时代的到来&#xff0c;国内数字化产品市场蓬勃发展&#xff0c;涌现出许多引领行业变革的产品。本文将介绍几个在数字孪生和人工智能领域取得突破的国内产品&#xff0c;带大家了解数字化产品的创新应用和影响力。 山海鲸可视化&#xff1a;山海鲸可视化是一款强大…