你可能已经在社交媒体上看到过n次关于pytorch和tensorflow的两极分化的争论。这些框架的普及推动了近年来深度学习的兴起。二者都不乏坚定的支持者,但在过去的一年里,一个明显的赢家已经开始出现。
pytorch是2018年最流行的框架之一。它已迅速成为学术界和工业界研究人员的首选深度学习框架。在过去几周使用了pytorch之后,我体会到它是一个非常灵活且易于使用的深度学习库。
在本文中,我们将探讨pytorch的全部内容。我们将不止学习理论-还包括编写4个不同的用例,看看pytorch的表现如何。建立深度学习模型从来没有这么有趣过!
注:本文假设你对深度学习概念已经有了基本的理解。如果没有,我建议阅读下文。
内容
什么是pytorch?
利用pytorch构建神经网络
用例1:手写数字分类(数字数据,mlp)
用例2:物体图像分类(图像数据,cnn)
用例3:情感文本分类(文本数据,rnn)
用例4:图像样式的迁移(迁移学习)
什么是pytorch?
在深入研究pytorch的实现之前,让我们先了解一下pytorch是什么,以及为什么它最近会变得如此流行。
pytorch是一个基于python的科学计算包,类似于numpy,它具备gpu附加功能。与此同时,它也是一个深度学习框架,为实现和构建深层神经网络体系结构提供了最大程度的灵活性和速度。
最近发布的pytorch 1.0帮助研究人员应对以下四大挑战:
大面积的返工
耗时的训练
python语言缺乏灵活性
慢速扩展
从本质上讲,pytorch与其他深度学习框架有两个不同点:
命令式编程
动态计算图
命令式编程:pytorch在遍历每一行代码的同时执行计算,这与python程序的执行方式非常类似,这一概念称为命令式编程,它的最大优点是可以动态地调试代码和编程逻辑。
动态计算图:pytorch被称为“由运行定义的”框架,这意味着计算图结构(神经网络体系结构)是在运行时生成的。该属性的主要优点是:它提供了一个灵活的编程运行时接口,通过连接操作来方便系统的构建和修改。在pytorch中,每个前向通路处定义一个新的计算图,这与使用静态图的tensorflow形成了鲜明的对比。
pytorch1.0附带了一个名为torch.jit的重要特性,它是一个高级编译器,允许用户分离模型和代码。此外,它还支持在定制硬件(如gpu或tpu)上进行有效的模型优化。
用pytorch构建神经网络
让我们通过一个实际案例来理解pytorch。学习理论固然好,但是如果你不把它付诸实践的话,它就没有多大用处了!
神经网络的pytorch实现看起来与numpy实现完全一样。本节的目标是展示pytorch和numpy的等效性质。为此,让我们创建一个简单的三层网络,在输入层中有5个节点,在隐藏层中有3个节点,在输出层中有1个节点。我们只使用一个带有五个特征和一个目标的单行训练示例。
import torchn_input, n_hidden, n_output = 5, 3, 1
第一步是进行参数初始化。这里,每个层的权重和偏置参数被初始化为张量变量。张量是pytorch的基本数据结构,用于建立不同类型的神经网络。可以将它们当作是数组和矩阵的推广,换句话说,张量是n维矩阵。
## initialize tensor for inputs, and outputsx = torch.randn((1, n_input))y = torch.randn((1, n_output))## initialize tensor variables for weightsw1 = torch.randn(n_input, n_hidden) # weight for hidden layerw2 = torch.randn(n_hidden, n_output) # weight for output layer## initialize tensor variables for bias termsb1 = torch.randn((1, n_hidden)) # bias for hidden layerb2 = torch.randn((1, n_output)) # bias for output layer
在参数初始化完成之后,可以通过以下四个关键步骤来定义和训练神经网络:
前向传播
损失计算
反向传播
更新参数
让我们更详细地了解每一个步骤。
前向传播:在这个步骤中,每个层都使用以下两个公式计算激活流。这些激活流从输入层流向输出层,以生成最终输出。
1. z = weight * input + bias2. a = activation_function (z)
下面的代码块显示了如何用pytorch编写这些步骤。请注意,大多数函数,如指数和矩阵乘法,均与numpy中的函数相类似。
## sigmoid activation function using pytorchdef sigmoid_activation(z): return 1 / (1 + torch.exp(-z))## activation of hidden layerz1 = torch.mm(x, w1) + b1a1 = sigmoid_activation(z1)## activation (output) of final layerz2 = torch.mm(a1, w2) + b2output = sigmoid_activation(z2)
损失计算:这一步在输出层中计算误差(也称为损失)。一个简单的损失函数可以用来衡量实际值和预测值之间的差异。稍后,我们将查看pytorch中可用的不同类型的损失函数。
loss = y - output
反向传播:这一步的目的是通过对偏差和权重进行边际变化,从而将输出层的误差降到最低,边际变化是利用误差项的导数计算出来的。
根据链规则的微积分原理,将增量变化返回到隐藏层,并对其权重和偏差进行相应的修正。通过对权重和偏差的调整,使得误差最小化。
## function to calculate the derivative of activationdef sigmoid_delta(x): return x * (1 - x)## compute derivative of error termsdelta_output = sigmoid_delta(output)delta_hidden = sigmoid_delta(a1)## backpass the changes to previous layersd_outp = loss * delta_outputloss_h = torch.mm(d_outp, w2.t())d_hidn = loss_h * delta_hidden
更新参数:最后一步,利用从上述反向传播中接收到的增量变化来对权重和偏差进行更新。
learning_rate = 0.1w2 += torch.mm(a1.t(), d_outp) * learning_ratew1 += torch.mm(x.t(), d_hidn) * learning_rateb2 += d_outp.sum() * learning_rateb1 += d_hidn.sum() * learning_rate
当使用大量训练示例对多个历元执行这些步骤时,损失将降至最小值。得到最终的权重和偏差值之后,用它对未知数据进行预测。
用例1:手写数字分类
在上一节中,我们看到了用pytorch编写神经网络的简单用例。在本节中,我们将利用pytorch提供的不同的实用程序包(nn、autograd、optimm、torchvision、torchtext等)来建立和训练神经网络。
利用这些包可以方便地定义和管理神经网络。在这个用例中,我们将创建一个多层感知器(mlp)网络,用于构建手写数字分类器。我们将使用torchvision包中的mnist数据集。
与你将要从事的任何项目一样,第一步是数据预处理:首先需要将原始数据集转换为张量,并在固定范围内将其归一化。torchvision包提供了一个名为transforms的实用程序,利用它可以将不同的转换组合在一起。
from torchvision import transforms_tasks = transforms.compose([ transforms.totensor(), transforms.normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ])
第一个转换是将原始数据转换为张量,第二个转换是通过以下操作执行归一化:
x_normalized = x-mean / std
数值为0.5,0.5表示红色、绿色和蓝色三个通道的均值和标准差。
from torchvision.datasets import mnist## load mnist dataset and apply transformationsmnist = mnist(data, download=true, train=true, transform=_tasks)
pytorch的另一个出色的实用工具是dataloader迭代器,它为多个处理器之间并行地批处理、搬移和加载数据提供了实现的可能。为了评估这个模型,我们将数据集划分为训练集和验证集。
from torch.utils.data import dataloaderfrom torch.utils.data.sampler import subsetrandomsampler## create training and validation splitsplit = int(0.8 * len(mnist))index_list = list(range(len(mnist)))train_idx, valid_idx = index_list[:split], index_list[split:]## create sampler objects using subsetrandomsamplertr_sampler = subsetrandomsampler(train_idx)val_sampler = subsetrandomsampler(valid_idx)## create iterator objects for train and valid datasetstrainloader = dataloader(mnist, batch_size=256, sampler=tr_sampler)validloader = dataloader(mnist, batch_size=256, sampler=val_sampler)
pytorch中的神经网络架构可以定义为一个类,这个类继承了称为module的nn包的基础类的所有属性。来自nn.module类的继承使得我们可以轻松地实现、访问和调用多个方法,还可以定义类的构造函数中的各个层,以及前向传播步骤中的前向函数。
我们将定义一个具有以下层配置的网络:[784,128,10]。此配置表示输入层中有784个节点(28*28像素)、隐藏层中有128个节点,输出层中有10个节点。在前向函数中,我们将在隐藏层(可以通过nn模块访问)中使用sigmoid激活函数。
import torch.nn.functional as fclass model(nn.module): def __init__(self): super().__init__() self.hidden = nn.linear(784, 128) self.output = nn.linear(128, 10) def forward(self, x): x = self.hidden(x) x = f.sigmoid(x) x = self.output(x) return xmodel = model()
利用nn和optim包定义损失函数和优化器:
from torch import optimloss_function = nn.crossentropyloss()optimizer = optim.sgd(model.parameters(), lr=0.01, weight_decay= 1e-6, momentum = 0.9, nesterov = true)
现在已经准备好,可以开始训练模型了,其核心步骤与前一节相同:前向传播、损失计算、反向传播和更新参数。
for epoch in range(1, 11): ## run the model for 10 epochs train_loss, valid_loss = [], [] ## training part model.train() for data, target in trainloader: optimizer.zero_grad() ## 1. forward propagation output = model(data) ## 2. loss calculation loss = loss_function(output, target) ## 3. backward propagation loss.backward() ## 4. weight optimization optimizer.step() train_loss.append(loss.item()) ## evaluation part model.eval() for data, target in validloader: output = model(data) loss = loss_function(output, target) valid_loss.append(loss.item()) print (epoch:, epoch, training loss: , np.mean(train_loss), valid loss: , np.mean(valid_loss))>> epoch: 1 training loss: 0.645777 valid loss: 0.344971>> epoch: 2 training loss: 0.320241 valid loss: 0.299313>> epoch: 3 training loss: 0.278429 valid loss: 0.269018>> epoch: 4 training loss: 0.246289 valid loss: 0.237785>> epoch: 5 training loss: 0.217010 valid loss: 0.217133>> epoch: 6 training loss: 0.193017 valid loss: 0.206074>> epoch: 7 training loss: 0.174385 valid loss: 0.180163>> epoch: 8 training loss: 0.157574 valid loss: 0.170064>> epoch: 9 training loss: 0.144316 valid loss: 0.162660>> epoch: 10 training loss: 0.133053 valid loss: 0.152957
完成了模型的训练之后,即可在验证数据基础上进行预测。
## dataloader for validation datasetdataiter = iter(validloader)data, labels = dataiter.next()output = model(data)_, preds_tensor = torch.max(output, 1)preds = np.squeeze(preds_tensor.numpy())print (actual:, labels[:10])print (predicted:, preds[:10])>>> actual: [0 1 1 1 2 2 8 8 2 8]>>> predicted: [0 1 1 1 2 2 8 8 2 8]
用例2:物体图像分类
现在让我们更进一步。
在这个用例中,我们将在pytorch中创建卷积神经网络(cnn)架构,利用流行的cifar-10数据集进行物体图像分类,此数据集也包含在torchvision包中。定义和训练模型的整个过程将与以前的用例相同,唯一的区别只是在网络中引入了额外的层。
加载并转换数据集:
## load the datasetfrom torchvision.datasets import cifar10cifar = cifar10('data', train=true, download=true, transform=_tasks)## create training and validation splitsplit = int(0.8 * len(cifar))index_list = list(range(len(cifar)))train_idx, valid_idx = index_list[:split], index_list[split:]## create training and validation sampler objectstr_sampler = subsetrandomsampler(train_idx)val_sampler = subsetrandomsampler(valid_idx)## create iterator objects for train and valid datasetstrainloader = dataloader(cifar, batch_size=256, sampler=tr_sampler)validloader = dataloader(cifar, batch_size=256, sampler=val_sampler)
我们将创建三个用于低层特征提取的卷积层、三个用于最大信息量提取的池化层和两个用于线性分类的线性层。
class model(nn.module): def __init__(self): super(model, self).__init__() ## define the layers self.conv1 = nn.conv2d(3, 16, 3, padding=1) self.conv2 = nn.conv2d(16, 32, 3, padding=1) self.conv3 = nn.conv2d(32, 64, 3, padding=1) self.pool = nn.maxpool2d(2, 2) self.linear1 = nn.linear(1024, 512) self.linear2 = nn.linear(512, 10) def forward(self, x): x = self.pool(f.relu(self.conv1(x))) x = self.pool(f.relu(self.conv2(x))) x = self.pool(f.relu(self.conv3(x))) x = x.view(-1, 1024) ## reshaping x = f.relu(self.linear1(x)) x = self.linear2(x) return xmodel = model()
定义损失函数和优化器:
import torch.optim as optimloss_function = nn.crossentropyloss()optimizer = optim.sgd(model.parameters(), lr=0.01, weight_decay= 1e-6, momentum = 0.9, nesterov = true)## run for 30 epochsfor epoch in range(1, 31): train_loss, valid_loss = [], [] ## training part model.train() for data, target in trainloader: optimizer.zero_grad() output = model(data) loss = loss_function(output, target) loss.backward() optimizer.step() train_loss.append(loss.item()) ## evaluation part model.eval() for data, target in validloader: output = model(data) loss = loss_function(output, target) valid_loss.append(loss.item())
完成了模型的训练之后,即可在验证数据基础上进行预测。
## dataloader for validation datasetdataiter = iter(validloader)data, labels = dataiter.next()output = model(data)_, preds_tensor = torch.max(output, 1)preds = np.squeeze(preds_tensor.numpy())print (actual:, labels[:10])print (predicted:, preds[:10])actual: ['truck', 'truck', 'truck', 'horse', 'bird', 'truck', 'ship', 'bird', 'deer', 'bird']pred: ['truck', 'automobile', 'automobile', 'horse', 'bird', 'airplane', 'ship', 'bird', 'deer', 'bird']
用例3:情感文本分类
我们将从计算机视觉用例转向自然语言处理,目的是展示pytorch在不同领域的不同应用。
在本节中,我们将利用基于rnn(递归神经网络)和lstm(长短期记忆)层的pyotch来完成文本分类任务。首先,加载包含两个字段(文本和目标)的数据集。目标包含两个类:class1和class2,我们的任务是将每个文本分为其中一个类。
可以在下面的链接中下载数据集。
https://s3-ap-south-1.amazonaws.com/av-blog-media/wp-content/uploads/2019/01/train.csv
train = pd.read_csv(train.csv)x_train = train[text].valuesy_train = train['target'].values
强烈建议在编码之前先设置种子,它可以保证你看到的结果与我的相同-这是在学习新概念时非常有用(也很有益)的特征。
np.random.seed(123)torch.manual_seed(123)torch.cuda.manual_seed(123)torch.backends.cudnn.deterministic = true
在预处理步骤中,首先将文本数据转换为tokens序列,之后便可以将其传递到嵌入层。我将利用keras包中提供的实用程序来进行预处理,利用torchtext包也同样可以实现。
from keras.preprocessing import text, sequence## create tokenstokenizer = tokenizer(num_words = 1000)tokenizer.fit_on_texts(x_train)word_index = tokenizer.word_index## convert texts to padded sequencesx_train = tokenizer.texts_to_sequences(x_train)x_train = pad_sequences(x_train, maxlen = 70)
接下来,需要将tokens转换成向量。为此,利用预先训练过的glove词嵌入。我们将加载这些单词嵌入,并创建一个包含单词向量的嵌入矩阵。
glove:
https://github.com/stanfordnlp/glove
embedding_file = 'glove.840b.300d.txt'embeddings_index = {}for i, line in enumerate(open(embedding_file)): val = line.split() embeddings_index[val[0]] = np.asarray(val[1:], dtype='float32')embedding_matrix = np.zeros((len(word_index) + 1, 300))for word, i in word_index.items(): embedding_vector = embeddings_index.get(word) if embedding_vector is not none: embedding_matrix[i] = embedding_vector
使用嵌入层和lstm层定义模型架构:
class model(nn.module): def __init__(self): super(model, self).__init__() ## embedding layer, add parameter self.embedding = nn.embedding(max_features, embed_size) et = torch.tensor(embedding_matrix, dtype=torch.float32) self.embedding.weight = nn.parameter(et) self.embedding.weight.requires_grad = false self.embedding_dropout = nn.dropout2d(0.1) self.lstm = nn.lstm(300, 40) self.linear = nn.linear(40, 16) self.out = nn.linear(16, 1) self.relu = nn.relu() def forward(self, x): h_embedding = self.embedding(x) h_lstm, _ = self.lstm(h_embedding) max_pool, _ = torch.max(h_lstm, 1) linear = self.relu(self.linear(max_pool)) out = self.out(linear) return outmodel = model()
创建训练和验证集:
from torch.utils.data import tensordataset## create training and validation splitsplit_size = int(0.8 * len(train_df))index_list = list(range(len(train_df)))train_idx, valid_idx = index_list[:split], index_list[split:]## create iterator objects for train and valid datasetsx_tr = torch.tensor(x_train[train_idx], dtype=torch.long)y_tr = torch.tensor(y_train[train_idx], dtype=torch.float32)train = tensordataset(x_tr, y_tr)trainloader = dataloader(train, batch_size=128)x_val = torch.tensor(x_train[valid_idx], dtype=torch.long)y_val = torch.tensor(y_train[valid_idx], dtype=torch.float32)valid = tensordataset(x_val, y_val)validloader = dataloader(valid, batch_size=128)
定义损失和优化器:
loss_function = nn.bcewithlogitsloss(reduction='mean')optimizer = optim.adam(model.parameters())
训练模型:
## run for 10 epochsfor epoch in range(1, 11): train_loss, valid_loss = [], []## training part model.train() for data, target in trainloader: optimizer.zero_grad() output = model(data) loss = loss_function(output, target.view(-1,1)) loss.backward() optimizer.step() train_loss.append(loss.item()) ## evaluation part model.eval() for data, target in validloader: output = model(data) loss = loss_function(output, target.view(-1,1)) valid_loss.append(loss.item())
最后得到预测结果:
dataiter = iter(validloader)data, labels = dataiter.next()output = model(data)_, preds_tensor = torch.max(output, 1)preds = np.squeeze(preds_tensor.numpy())actual: [0 1 1 1 1 0 0 0 0]predicted: [0 1 1 1 1 1 1 1 0 0]
用例4:图像样式迁移
让我们来看最后一个用例,在这里我们将执行图形样式的迁移。这是我经历过的最有创意的项目之一,希望你也能玩得开心。样式迁移概念背后的基本理念是:
从一幅图像中获取对象/内容
从另一幅图像中获取样式/纹理
生成二者混合的最终图像
“利用卷积网络进行图像样式迁移”这篇论文中对这一概念做了介绍,样式迁移的一个例子如下:
太棒了,对吧?让我们看看它在pytorch中是如何实现的。这一进程包括六个步骤:
从两个输入图像中提取低层特征。这可以使用vgg 19这样的预训练的深度学习模型。
from torchvision import models# get the features portion from vgg19vgg = models.vgg19(pretrained=true).features# freeze all vgg parametersfor param in vgg.parameters(): param.requires_grad_(false)# check if gpu is availabledevice = torch.device(cpu)if torch.cuda.is_available(): device = torch.device(cuda)vgg.to(device)
将这两幅图像加载到设备上,并从vgg中获取特征。另外,也可以应用以下转换:调整张量的大小,以及值的归一化。
from torchvision import transforms as tfdef transformation(img): tasks = tf.compose([tf.resize(400), tf.totensor(), tf.normalize((0.44,0.44,0.44),(0.22,0.22,0.22))]) img = tasks(img)[:3,:,:].unsqueeze(0) return imgimg1 = image.open(image1.jpg).convert('rgb')img2 = image.open(image2.jpg).convert('rgb')img1 = transformation(img1).to(device)img2 = transformation(img2).to(device)
现在,我们需要获得这两幅图像的相关特征。从第一个图像中,我们需要提取内容或与存在的对象相关的特征;从第二张图像中,我们需要提取与样式和纹理相关的特征。
对象相关特征:在最初的文章中,作者建议可以从网络的初始层中提取更有价值的对象和内容,这是因为在较高层上,信息空间变得更为复杂,像素信息细节在高层往往会丢失。
样式相关特征:为了从第二幅图像中获取样式和纹理信息,作者在不同层次上使用了不同特征之间的相关性,下文第4点对此作了详细解释。
在实现这一目标之前,让我们来看看一个典型的vgg 19模型的结构:
对象信息提取用到的是conv42层,它位于第4个卷积块中,深度为512。对于样式的表达,用到的层是网络中每个卷积块的第一卷积层,即conv11、conv21、conv31、conv41和conv51,这些层的选取纯粹是根据作者的经验来做出选择,我仅在本文中复制它们的结果。
def get_features(image, model): layers = {'0': 'conv1_1', '5': 'conv2_1', '10': 'conv3_1', '19': 'conv4_1', '21': 'conv4_2', '28': 'conv5_1'} x = image features = {} for name, layer in model._modules.items(): x = layer(x) if name in layers: features[layers[name]] = x return featuresimg1_features = get_features(img1, vgg)img2_features = get_features(img2, vgg)
正如前面提到的,作者使用不同层次的相关性来获得与样式相关的特征。这些特征的相关性由gram矩阵g给出,其中g中的每个单元(i,j)都是层中向量特征映射i和j之间的内积。
def correlation_matrix(tensor): _, d, h, w = tensor.size() tensor = tensor.view(d, h * w) correlation = torch.mm(tensor, tensor.t()) return correlationcorrelations = {l: correlation_matrix(img2_features[l]) for l in img2_features}
最终,可以利用这些特征和相关性进行样式转换。现在,为了将样式从一个图像转换到另一个图像,需要设置用于获取样式特征的每一层的权重。如上所述,由于初始层提供了更多的信息,因此可以为初始层设置更高的权重。此外,定义优化器函数和目标图像,也即是图像1的副本。
weights = {'conv1_1': 1.0, 'conv2_1': 0.8, 'conv3_1': 0.25, 'conv4_1': 0.21, 'conv5_1': 0.18}target = img1.clone().requires_grad_(true).to(device)optimizer = optim.adam([target], lr=0.003)
启动损失最小化处理过程:即在循环中运行大量步骤,来计算与对象特征提取和样式特征提取相关的损失。利用最小化后的损失,更新网络参数,进一步修正目标图像。经过一些迭代之后,将生成更新后的图像。
for ii in range(1, 2001): ## calculate the content loss (from image 1 and target) target_features = get_features(target, vgg) loss = target_features['conv4_2'] - img1_features['conv4_2'] content_loss = torch.mean((loss)**2) ## calculate the style loss (from image 2 and target) style_loss = 0 for layer in weights: target_feature = target_features[layer] target_corr = correlation_matrix(target_feature) style_corr = correlations[layer] layer_loss = torch.mean((target_corr - style_corr)**2) layer_loss *= weights[layer] _, d, h, w = target_feature.shape style_loss += layer_loss / (d * h * w) total_loss = 1e6 * style_loss + content_loss optimizer.zero_grad() total_loss.backward() optimizer.step()
最后,我们可以看到预测的结果,在这里我只运行了一小部分迭代,还可以运行多达3000次迭代(如果计算资源足够多的话!)。
def tensor_to_image(tensor): image = tensor.to(cpu).clone().detach() image = image.numpy().squeeze() image = image.transpose(1, 2, 0) image *= np.array((0.22, 0.22, 0.22)) + np.array((0.44, 0.44, 0.44)) image = image.clip(0, 1) return imagefig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))ax1.imshow(tensor_to_image(img1))ax2.imshow(tensor_to_image(target))
后记
pytorch还可以实现大量的其他用例,它很快成为全球研究人员的宠儿。绝大多数pytorch实现的开源库和开发应用可以在github上看到。
在本文中,我阐述了什么是pytorch,以及如何用pytorch实现不同的用例,当然,这个指南只是一个出发点。如果能提供更多的数据,或进行更多的网络参数微调,那么每个用例的性能都可以得到大幅度提高,最重要的是如果在构建网络体系架构时应用创新技能,也能提高用例的性能。感谢你的阅读,并请在下面的评论部分留下你的反馈。
参考文献
1. 官方pytorch指南:
https://pytorch.org/tutorials/
2. 用pytorch进行深度学习
https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html
3. faizan在analytics vidhya上发表的文章:
https://www.analyticsvidhya.com/blog/2018/02/pytorch-tutorial/
4. 使用pytorch的udacity深度学习:
https://github.com/udacity/deep-learning-v2-pytorch
5. 图片样式迁移原始论文:
https://www.cvfoundation.org/openaccess/content_cvpr_2016/papers/gatys_image_style_transfer_cvpr_2016_paper.pdf
你还可以在analytics vidhya的安卓app上阅读本文。
Agilent安捷伦N5767A直流电源60V
磁带存储会消失吗
OPPO第二代AR眼镜产品:OPPO AR Glass 2021正式和大家见面
关于电阻器的简单介绍
曝vivo即将发布首款弹出式全面屏的865旗舰 且延续NEX3瀑布屏方案
教你用PyTorch快速准确地建立神经网络
中新泰合“年产8000吨芯片封装材料生产线建设项目”投产
激光雷达能在哪些场景下发挥优势
mifoi2全能耳机体验 功能很全面很强大
为什么要使用Redis做缓存?
回顾2018自动化及智能制造市场状况
鸿海大陆厂区复工比例超季节性需求50%
具有R-R输入功能的低压低功耗CMOS运算放大电路的设计与实现
区块链5.0的到来意味着什么
苹果申请新专利,或将解决OLED屏幕烧屏问题
DIGITIMES:2023年至2028年全球笔记本电脑出货量复合年增长率为3%
vr技术的优势在哪里
满足智能汽车需求的新一代车用处理器出笼
采用机器视觉技术实现导览机器人控制系统总方案及软硬件设计
74HC046/74HCT4046各引脚功能及管脚电压