目录

  • 消息传递
    • MessagePassing Base Class
    • 简单GCN层的实现
    • 实现边卷积(Edge Convolution)
  • 创建自定义数据集
    • 创建"In Memory Datasets"
    • 创建 "Larger" Datasets
    • 常见问题

消息传递

这部分内容主要是了解"消息传递网络"(Message passing networks)的创建;

将卷积操作推广到不规则域后被称为邻域聚合(neighborhood aggregation)或消息传递(message passing)。我们定义:

  • xi(k−1)∈RFx_{i}^{(k-1)}\in R^{F}xi(k1)RF代表第k−1k-1k1层Graph中节点iii的特征;
  • ej,i∈RDe_{j,i}\in R^{D}ej,iRD代表节点jjj连接到节点iii的边特征;

而消息传递网络可以被描述为:xi(k)=γ(k)(xi(k−1),Funcj∈N(i)ϕ(k)(xi(k−1),xj(k−1),ej,i))x_{i}^{(k)}=\gamma^{(k)}(x^{(k-1)}_{i},Func_{j\in N(i)}\phi^{(k)}(x^{(k-1)}_{i},x^{(k-1)}_{j},e_{j,i}))xi(k)=γ(k)(xi(k1),FuncjN(i)ϕ(k)(xi(k1),xj(k1),ej,i))其中,FuncFuncFunc表示一个可微分,并具有可置换性的函数,比如meansummaxγ\gammaγ(更新函数)和ϕ\phiϕ(消息传递函数)表示可微分的函数。

MessagePassing Base Class

PyG中提供了实现消息传递机制的类torch_geometric.nn.MessagePassing,我们只需要重新定义以下函数就能实现自己的消息传递模型:ϕ\phiϕmessage(),函数γ\gammaγupdate(),aggregation 机制(aggr="add"aggr="mean"aggr="max");

在消息传递模型的构建中,我们需要认识以下四个方法:

  • torch_geometric.nn.MessagePassing(aggr="add",flow="source_to_target"):用于初始化,用于定义聚合方式,以及信息传递的方向;
  • torch_geometric.nn.MessagePassing.propagate(edge_index,size=None,**kwargs):函数用于传播消息的初始化调用,更新节点的embedding表达;
    其中,**kwargs是不定长关键字参数,用于构建与聚合消息以及节点embedding更新所需的任何附加数据,**kwargsmessage()的参数相关;
  • torch_geometric.nn.MessagePassing.message(x_j:'torch.Tensor')->'torch.Tensor':构建消息从节点jjj向节点iii传递消息,代表函数ϕ\phiϕ,即在edge_index中的每条边上调用ϕ\phiϕ
    另外,对于边的表示有以下规则:如果flow="source_to_target"(j,i)(j,i)(j,i),如果flow="target_to_source"(i,j)(i,j)(i,j)
    此函数可以将最初传递给propagate()的初始输入作为它的参数;
    最后注意一个关于该函数的特点,我们可以通过把_i_j附加到变量名称x后(比如x_ix_j),从而将传递给propagate()的张量映射到对应的节点iii和节点jjjx_i代表当前节点,x_j代表当前节点的邻居节点;
  • torch_geometric.nn.MessagePassing.update(inputs:'torch.Tensor')->'torch.Tensor':更新节点的embedding,代表函数γ\gammaγ,即对每个节点都调用γ\gammaγ

调用propagate(), 内部会自动调用message()update()


通过以上内容,结合:xi(k)=γ(k)(xi(k−1),Funcj∈N(i)ϕ(k)(xi(k−1),xj(k−1),ej,i))x_{i}^{(k)}=\gamma^{(k)}(x^{(k-1)}_{i},Func_{j\in N(i)}\phi^{(k)}(x^{(k-1)}_{i},x^{(k-1)}_{j},e_{j,i}))xi(k)=γ(k)(xi(k1),FuncjN(i)ϕ(k)(xi(k1),xj(k1),ej,i))我们可以发现,MessagePassing.__init__()用于定义Funcj∈N(i)Func_{j\in N(i)}FuncjN(i)message()用于定义ϕ(k)(xi(k−1),xj(k−1),ej,i)\phi^{(k)}(x^{(k-1)}_{i},x^{(k-1)}_{j},e_{j,i})ϕ(k)(xi(k1),xj(k1),ej,i)update()用于定义γ(k)\gamma^{(k)}γ(k)

另外,当我们在实现自定义MessagePassing模型时,我们只需设计当前节点iii的消息聚合过程,因为调用update后,程序会自动处理到每个节点。

简单GCN层的实现

模型来自"Semi-Supervised Classification with Graph Convolutional Networks";

一般GCN层被定义为:xi(k)=∑j∈N(i)∪{i}1deg(i)⋅deg(j)⋅(Θ⋅xj(k−1))x^{(k)}_{i}=\sum_{j\in N(i)\cup \left\{i\right\}}\frac{1}{\sqrt{deg(i)}\cdot\sqrt{deg(j)}}\cdot(\Theta\cdot x^{(k-1)}_{j})xi(k)=jN(i){i}deg(i)

deg(j)

1(Θxj(k1))其中,xi(k)x^{(k)}_{i}xi(k)代表第kkk层GCN输出graph的第iii个节点的特征,节点与邻居节点的特征由权重矩阵Θ\ThetaΘ转换,按照节点的度进行归一化,1deg(i)⋅deg(j)\frac{1}{\sqrt{deg(i)}\cdot\sqrt{deg(j)}}deg(i)

deg(j)

1
是节点iii的归一化系数,最后聚合信息,具体步骤如下:

  • 在邻接矩阵上添加自循环;
  • 线性变换节点特征矩阵;
  • 计算归一化系数;
  • ϕ\phiϕ中归一化节点特征;
  • 聚合节点与邻居节点的信息;

在Base Class中,Funcj∈N(i)Func_{j\in N(i)}FuncjN(i)的计算对象不包括当前节点本身,但GCN层需要考虑节点自身信息(j∈N(i)∪{i}j\in N(i)\cup \left\{i\right\}jN(i){i}),因此我们需要增加节点自环的计算过程(add_self_loops);

GCN层的实现如下:

import torch
from torch_geometric.nn import MessagePassing
from torch_geometric.utils import add_self_loops,degree
from torch_geometric.data import Dataclass GCNConv(MessagePassing):def __init__(self,in_channels,out_channels):super().__init__(aggr='add') # Add aggregationself.lin=torch.nn.Linear(in_channels,out_channels)def forward(self,x,edge_index):# x的shape [N,in_channels] N:节点数# edge_index的shape [2,E] E:边的数量# 在邻接矩阵上添加节点自环edge_index,_=add_self_loops(edge_index,num_nodes=x.size(0)) # [2,E]# 线性变换特征矩阵x=self.lin(x) # [N,out_channels]# 归一化计算row,col=edge_index # 解包 row的shape[E], col的shape[E]"""degree(index,num_nodes=None,dtype=None)index是data.edge_index中的任意一行, 作用是计算每个节点出现的次数"""deg=degree(index=col,num_nodes=x.size(0),dtype=x.dtype) # 计算每个节点的度, deg shape [N]deg_inv_sqrt=deg.pow(-0.5) # 为每个节点的度开根并求倒数 deg_inv_sqrt shape [N]deg_inv_sqrt[deg_inv_sqrt==float('inf')]=0norm=deg_inv_sqrt[row]*deg_inv_sqrt[col] # row和col存储的是边索引, 对于第m条边, 可用节点row[m]到节点col[m]表达# norm shape [E]"""调用propagate(), 内部会自动调用message()和update(), 传递的参数是x"""# 聚合节点与邻居节点的信息return self.propagate(edge_index,x=x,norm=norm) # [N,out_channels]def message(self,x_j,norm):# 归一化邻域内节点的特征# x_j的shape [E,out_channels]# norm.view(-1,1) shape [E,1]# norm.view(-1,1)*x_j 将每条边的norm系数缩放到特征的所有通道上return norm.view(-1,1)*x_j

注意message的定义:

def message(self,x_j,norm):# 归一化邻域内节点的特征# x_j的shape [E,out_channels]# norm.view(-1,1) shape [E,1]# norm.view(-1,1)*x_j 将每条边的norm系数缩放到特征的所有通道上return norm.view(-1,1)*x_j

由于在forward中,x已经被线性变换过,所以参数x_j中的元素即为数学定义下的Θ⋅xj(k−1)\Theta\cdot x_{j}^{(k-1)}Θxj(k1),而norm中保存的就是归一化系数1deg(i)⋅deg(j)\frac{1}{\sqrt{deg(i)}\cdot\sqrt{deg(j)}}deg(i)

deg(j)

1

关于x_j shape [E,out_channels]norm shape [E]可以这样理解,本身消息传递函数的意义就是:构建消息从节点jjj向节点iii传递消息,也就是我们要在edge_index中的每条边上调用ϕ\phiϕ,因此x_j是graph中所有要计算的节点iii各自对应的邻居节点jjj组成的特征数组。


另外补充内容:广播

上面计算中出现了[E,1]*[E,out_channels]的形式,这其实是利用了广播的规则;广播可以减少代码量。

简单来说,如果有一个m×nm\times nm×n的矩阵,让它加减乘除一个1×n1\times n1×n的矩阵,它会被复制mmm次,成为一个m×nm\times nm×n的矩阵,然后再逐元素地进行加减乘除操作。同样地这对m×1m\times 1m×1的矩阵成立。


定义一个图,输入模型:

"""
创建图, 每个节点的特征维数为1:
边的邻接矩阵为:
[[0,1,0],[1,0,1],[0,1,0]]
"""
edge_index=torch.tensor([[0,1,1,2],[1,0,2,1]],dtype=torch.long)x=torch.tensor([[-1],[0],[1]],dtype=torch.float)data=Data(x=x,edge_index=edge_index)print(data) # Data(edge_index=[2, 4], x=[3, 1])# 初始化与调用
conv=GCNConv(1,3)x=conv(data.x,data.edge_index)

实现边卷积(Edge Convolution)

模型来自"Dynamic Graph CNN for Learning on Point Clouds",边卷积层通常用于处理graph或者点云数据(point clouds),数学定义为:xi(k)=maxj∈N(i)hΘ(xi(k−1),xj(k−1)−xi(k−1))x_{i}^{(k)}=max_{j\in N(i)}h_{\Theta}(x_{i}^{(k-1)},x_{j}^{(k-1)}-x_{i}^{(k-1)})xi(k)=maxjN(i)hΘ(xi(k1),xj(k1)xi(k1))其中,hΘh_{\Theta}hΘ代表多层感知机(MLP),注意hΘ(m,n)h_{\Theta}(m,n)hΘ(m,n)是先拼接m,nm,nm,n再进行变换,即hΘ(m⊕n)h_{\Theta}(m\oplus n)hΘ(mn)

类比 GCN 层,我们可以使用 MessagePassing 类来实现这一层,这次使用"max"聚合:

import torch
import torch.nn as nn
from torch_geometric.nn import MessagePassing
from torch_geometric.data import Dataclass EdgeConv(MessagePassing):def __init__(self,in_channels,out_channels):super().__init__(aggr='max') # "max" aggregationself.mlp=nn.Sequential(nn.Linear(2*in_channels,out_channels),nn.ReLU(),nn.Linear(out_channels,out_channels))def forward(self,x,edge_index):# x shape [N,in_channels] N:节点数# edge_index shape [2,E] E:边的数量return self.propagate(edge_index,x=x) # [N,out_channels]def message(self,x_i,x_j):# x_i shape [E,in_channels]# x_j shape [E,in_channels]tmp=torch.cat([x_i,x_j-x_i],dim=-1) # [E,2* in_channles]return self.mlp(tmp)

message的参数中,我们用x_i内的元素代表当前节点的特征,x_j内的元素代表邻居节点的特征;

同样的,我们对节点与邻居节点调用ϕ\phiϕ,等价于在每条边上调用ϕ\phiϕ,所以有x_i shape [E,in_channels]x_j shape [E,in_channels]

创建自定义数据集

尽管 PyG 已经包含很多有用的数据集,但我们希望使用自行记录或非公开可用的数据创建自己的数据集。下面,简要介绍一下设置自己的数据集所需的条件。

关于数据集,存在两个类:torch_geometric.data.Datasettorch_geometric.data.InMemoryDataset

torch_geometric.data.InMemoryDataset 继承自 torch_geometric.data.Dataset 并且应该在整个数据集可以被 CPU 完全加载到内存时使用。

遵循 torchvision 的约定,每个数据集都会传递一个根文件夹(root folder),该文件夹指示数据集应该存储的位置。我们把根文件夹分成两个文件夹:

  • raw_dir,数据集下载后的保存位置;
  • processed_dir,经过一些简单整理后的数据集保存位置。

此外,每个数据集都可以传递到transformpre_transformpre_filter 函数,默认情况下它们是None

transform函数在访问之前动态转换数据对象(因此它最好用于数据增强);

pre_transform 函数在将数据对象保存到磁盘之前应用转换(因此它最好用于只需要执行一次的繁重预计算);

pre_filter 函数可以在保存前手动过滤掉数据对象;

创建"In Memory Datasets"

为了创建 torch_geometric.data.InMemoryDataset,需要实现四个基本方法:

  • torch_geometric.data.InMemoryDataset.raw_file_names():返回一个文件列表,包含了raw_dir中的文件目录,可以根据该列表来决定哪些需要下载或者已下载就跳过;
  • torch_geometric.data.InMemoryDataset.processed_file_names():返回一个处理后的文件列表,包含processed_dir中的文件目录,根据列表内容决定哪些数据已经处理过从而可以跳过处理;
  • torch_geometric.data.InMemoryDataset.download():下载原始数据到raw_dir
  • torch_geometric.data.InMemoryDataset.process():处理原始数据并存放到processed_dir

神奇的事情发生在process()方法,在这里,我们读取并创建一个Data对象列表并将其保存到processed_dir中,由于保存一个巨大的python列表很慢,我们在保存之前通过torch_geometric.data.InMemoryDataset.collate()将列表整理成一个巨大的Data对象,整理后的数据对象将所有实例连接成一个大的数据对象,并且返回一个slices字典以便于重建单个实例。最后,我们需要在构造函数中将这两个对象加载到属性self.dataself.slices

下面使用一个简单的格式熟悉这个过程:

import torch
from torch_geometric.data import InMemoryDataset,download_urlclass MyOwnDataset(InMemoryDataset):def __init__(self,root,transform=None,pre_transform=None):super().__init__(root,transform,pre_transform)self.data,self.slices=torch.load(self.processed_paths[0])@propertydef raw_file_names(self):return ['some_file_1','some_file_2',...]@propertydef processed_file_names(self):return ['data1.pt','data2.pt',...]def download(self):# 下载到 self.raw_dirdownload_url('url',self.raw_dir)def process(self):# 将数据整合到一个很大的Data列表里data_list=[...]if self.pre_filter is not None:data_list=[data for data in data_list if self.pre_filter(data)]if self.pre_transform is not None:data_list=[self.pre_transform(data) for data in data_list]data,slices=self.collate(data_list)torch.save((data,slices),self.processed_paths[0])

首先注意,property是一个特殊的装饰器该装饰器(也是一个数据描述符)将类中的读写方法变成了一种属性读写的控制,通过property修饰,方法调用被转为属性一样的读写。比如self.data,self.slices=torch.load(self.processed_paths[0])中的self.processed_paths[0],像属性一样的对象processed_paths其实是一个继承自class Dataset的方法。

关于self.processed_paths,其返回的是一个文件列表,返回内容由processed_file_names返回的文件列表决定。self.processed_paths[0]则是第一个已处理数据的位置,即dir+data1.pt。回顾前面的内容,每个数据实例包含两部分:dataslices


torch.load()可以加载使用torch.save()保存的对象,所以可以看到torch.save((data,slices),self.processed_paths[0]),我们保存了两个对象dataslicesdir+data1.pt里;


现在,我们将 Cora 应用到 In Memory Datasets 中:

import torch
from torch_geometric.data import InMemoryDataset
from torch_geometric.data import download_url
import os
from torch_geometric.io import read_planetoid_dataclass MyOwnDataset(InMemoryDataset):def __init__(self,url= 'https://github.com/kimiyoung/planetoid/raw/master/data',root='MyOwnDataset',transform=None,pre_transform=None,pre_filter=None):self.url=urlself.transform=transformself.pre_filter=pre_filterself.pre_transform=pre_transformself.raw=os.path.join(root,'raw')self.processed=os.path.join(root,'processed')super().__init__(root=root, transform=transform, pre_transform=pre_transform, pre_filter=pre_filter)print(self.processed_paths)self.x, self.slices = torch.load(self.processed_paths[0])# 返回原始文件列表@propertydef raw_file_names(self):names = ['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index']return ['ind.cora.{}'.format(name) for name in names]# 返回需要跳过的文件列表@propertydef processed_file_names(self):return ['data.pt','pre_transform.pt','pre_filter.pt']# 下载原始数据def download(self):for name in self.raw_file_names:download_url('{}/{}'.format(self.url, name), self.raw)def process(self):# read_planetoid_data(folder, prefix) 加载数据集并划分训练,验证,测试集data=read_planetoid_data(self.raw,'cora')data_list = [data]print(data_list)if self.pre_filter is not None:data_list = [data for data in data_list if self.pre_filter(data)]if self.pre_transform is not None:data_list = [self.pre_transform(data) for data in data_list]data, slices = self.collate(data_list)torch.save((data, slices), self.processed_paths[0])"""
初始化实例时, 先调用super().__init__(), 此时中断进入download(), 然后执行process(), 最终完成整个__init__()的执行
其中, download()与process()是否可以跳过与raw_file_names, processed_file_names相关
"""
data=MyOwnDataset()
print(data.x)
print(data.slices)
"""
下载部分省略Processing...
[Data(edge_index=[2, 10556], test_mask=[2708], train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])]
Done!['MyOwnDataset\\processed\\data.pt', 'MyOwnDataset\\processed\\pre_transform.pt', 'MyOwnDataset\\processed\\pre_filter.pt']Data(edge_index=[2, 10556], test_mask=[2708], train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708]){'x': tensor([   0, 2708]), 'edge_index': tensor([    0, 10556]), 'y': tensor([   0, 2708]), 'train_mask': tensor([   0, 2708]),
'val_mask': tensor([   0, 2708]), 'test_mask': tensor([   0, 2708])}
"""

可以看出,data_list的内容为[Data(edge_index=[2, 10556], test_mask=[2708], train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])],这个巨大的Data对象包含了各种实例数据,当然列表里还可以接着存放其他Data对象。

并且得到以下形式的目录:

创建 “Larger” Datasets

上面的 In Memory Datasets 可以通过内存完成全部加载,所以通常是较小型的数据集。为了创建内存不能一次性加载的数据集,可以使用torch_geometric.data.Dataset,它遵循torchvision的概念,使用时需要实现以下方法:

  • torch_geometric.data.Dataset.len():返回数据集中的实例数;
  • torch_geometric.data.Dataset.get():实现加载单个图的功能;

在内部,torch_geometric.data.Dataset.__getitem__()torch_geometric.data.Dataset.get() 获取数据对象,并可选择transform对它们进行转换;

下面通过一个简化例子感受:

import os.path as ospimport torch
from torch_geometric.data import Dataset,download_urlclass MyOwnDataset(Dataset):def __init__(self,root,transform=None,pre_transform=None):super().__init__(root,transform,pre_transform)@propertydef raw_file_names(self):return ['some_file_1','some_file_2',...]@propertydef processed_file_names(self):return ['data_1.pt','data_2.pt',...]def download(self):download_url('url',self.raw_dir)def process(self):i=0for raw_path in self.raw_paths:# 从raw_path读数据data=Data(...)if self.pre_filter is not None and not self.pre_filter(data):continueif self.pre_transform is not None:data=self.pre_transform(data)torch.save(data,osp.join(self.processed_dir,'data_{}.pt'.format(i)))i += 1def len(self):return len(self.processed_file_names)def get(self,idx):data=torch.load(osp.join(self.processed_dir,'data_{}.pt'.format(idx)))return data

在这里,每个图数据对象都通过process()进行单独保存,我们可以通过get()手动加载某个具体图数据。

常见问题

1.如何完全跳过 download()process() 的执行?

我们可以通过不覆盖(not overriding) download()process() 方法来跳过下载或处理:

class MyOwnDataset(Dataset):def __init__(self, transform=None, pre_transform=None):super().__init__(None, transform, pre_transform)

2.我真的需要使用这些数据集接口(interfaces)吗?

不用,其实就像在 PyTorch 中一样,例如,当我想动态创建合成数据而不将它们显式保存到磁盘时。 在这种情况下,只需传递一个包含 torch_geometric.data.Data 对象的常规 python 列表并将它们传递给 torch_geometric.loader.DataLoader

from torch_geometric.data import Data
from torch_geometric.loader import DataLoaderdata_list = [Data(...), ..., Data(...)]
loader = DataLoader(data_list, batch_size=32)

第十七课.Pytorch-geometric入门(二)相关推荐

  1. 第二十七课.深度强化学习(二)

    目录 概述 价值学习 Deep Q Network DQN的训练:TD算法(Temporal Difference Learning) 策略学习 Policy Network 策略网络训练:Polic ...

  2. PyG:PyTorch Geometric Library

    PyG是一个基于PyTorch用与处理部规则数据(比如图)的库,是一个用于在图等数据上快速实现表征学习的框架,是当前最流行和广泛使用的GNN(Graph Neural Networks, GNN 图神 ...

  3. 智课雅思词汇---二十七、形容词后缀-ant/-ent

    智课雅思词汇---二十七.形容词后缀-ant/-ent 一.总结 一句话总结: ...的 后缀:-ant ①[形容词后缀] 大部分与-ance或-ancy,相对应,表示属于...的.具有...性质的 ...

  4. Pytorch的入门操作(二)

    2.Pytorch 2.1 Pytorch的介绍和安装 目标: 知道如何安装Pytorch 2.1.1 Pytorch的介绍 Pytorch是Facebook发布的深度学习框架,由其易用性,友好性,深 ...

  5. 实践数据湖iceberg 第三十七课 kakfa写入iceberg的 icberg表的 enfource ,not enfource测试

    系列文章目录 实践数据湖iceberg 第一课 入门 实践数据湖iceberg 第二课 iceberg基于hadoop的底层数据格式 实践数据湖iceberg 第三课 在sqlclient中,以sql ...

  6. NeHe OpenGL教程 第四十七课:CG顶点脚本

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  7. 新手第四课-PaddlePaddle快速入门

    新手第四课-PaddlePaddle快速入门 文章目录 新手第四课-PaddlePaddle快速入门 PaddlePaddle基础命令 计算常量的加法:1+1 计算变量的加法:1+1 使用Paddle ...

  8. 在斜坡上哪个物体滚的最快_人教版一年级上册 第十七课 会滚的玩具

    同学们好,欢迎来到美术课堂!你们喜欢会滚的玩具吗?你们了解它们滚动的原理吗?今天我们就来一起制作会滚的玩具,一起来了解它们背后的小秘密吧! 教材展示 教案参考 01 教材分析 <会滚的玩具> ...

  9. 带你少走弯路:强烈推荐的Pytorch快速入门资料和翻译(可下载)

    上次写了TensorFlow的快速入门资料,受到很多好评,读者强烈建议我出一个pytorch的快速入门路线,经过翻译和搜索网上资源,我推荐3份入门资料,希望对大家有所帮助. 备注:TensorFlow ...

  10. python编程入门第一课_python入门前的第一课 python怎样入门

    人工智能时代的到来,很多文章说这么一句:"不会python,就不要说自己是程序员",这说的有点夸张了,但确实觉得目前python这个语言值得学习,而且会python是高薪程序员的必 ...

最新文章

  1. 影像组学视频学习笔记(15)-ROC曲线及其绘制、Li‘s have a solution and plan.
  2. 2.搭建cassandra时遇到没有公网网卡的问题
  3. 用神经网络的分类行为理解质量到底是什么?
  4. hdu 1081To The Max
  5. 萌娃六一对程序员老爸说:再不陪我玩我就长大了
  6. XmlHelpers
  7. 面向对象编程设计练习题(2)
  8. eclipse Android开发——布局查看
  9. 中望3d快捷键命令大全_cad快捷键命令大全
  10. IDEA代码格式化快捷键
  11. eclipse配置jsp页面模板
  12. YAML文件格式详解
  13. 转:MOSS 中的计算公式
  14. 贴片电阻分类、阻值、功率、封装、尺寸
  15. 【C语言】十进制转换二进制
  16. taylor+swift纽约公寓_Taylor Swift $1,800 万美元的纽约豪宅到底豪在哪里?
  17. matlab中gurobi lic file 打不开
  18. 【鸿蒙OS开发入门】13 - 启动流程代码分析之第一个用户态进程:init 进程 之 init 任务详解
  19. 通信业的双11来了!充话费、办宽带、买手机每年这时候最划算
  20. 【Java】Java的垃圾回收

热门文章

  1. Springboot之YAML语法
  2. Linux权限管理(suid euid)
  3. 简洁好用的数据库表结构文档生成工具!
  4. 你听过BA、DA、AA、TA么?全网疯传的架构实践全景图!
  5. 双十一秒杀架构模型设计实践!
  6. 实战篇:一个核心系统 3 万多行代码的重构之旅
  7. 五分钟看懂抓包神技:DPDK
  8. 典型云平台技术栈有哪些?
  9. Angular动态创建组件之Portals
  10. 一些能说到点子上的课程