pytorch张量运算

pytorch张量运算

使用cpu gpu训练数据

torch.cuda.is_available() # 判断机器支不支持cpu

一般用如下命令:

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
fm = fm.to(device) # fm是定义的module
batch_x = batch_x.to(device) # 将数据转化为device

2.1 数据操作

  • 深度学习落实到计算表现为矩阵计算
  • pytorch、tensorflow中,计算的基本组件是Tensor。张量即多维数组,是矩阵计算的基本单元。
  • Tensor:张量,一维张量即向量vector,二维张量即 二维数组。张量是n维数组的统称
  • python中有专门进行矩阵计算的库:numpy。pytorch和tensorflow等 和 numpy除了矩阵计算的相似点之外,多了如下额外的功能:
    • 张量支持GPU加速;numpy仅仅支持CPU
    • 张量支持自动微分,可以自动进行求导

2.1.1 创建张量

创建向量

x = torch.arrange(12, dtype=torch.float) # 创建vector,除非额外指定,否则其存储于GPU中
x = x.reshape(3,4) # 改变tensor的形状
x = torch.zeros((2,3,4)) # 创建全0向量
x = torch.ones((2,3,4)) # 创建全1向量
x = torch.randn(3,4) # 创建 均值为0,方差为1的标准高斯分布
x = torch.tensor([[2,1,4,3],[1,2,3,4],[4,3,2,1]]) # 将list转化为tensor
x = torch.full((2,2),3) # 创建 向量维度大小为(2,2),所有数值为3的向量

从列表、numpy、数组创建tensor

x=torch.tensor([1,2,3],dtype=torch.float) # 从数组创建
x=torch.as_tensor((1,2,3),dtype=torch.float) # 从tuple创建
x = torch.from_numpy(np.random.randn(3)) # 从numpy数组

pytorch中的tensor有如下四种数据类型

  • 浮点型:torch.float32(torch.float) , torch.float64(或者torch.double)
  • 整型: yotch.int8,torch.int16, torch.int32(torch.int),torch.int64(torch.long)
  • 布尔型:torch.bool
  • 其他: torch.complex64, torch.complex128

通过tensor.dtype查看tensor的数据类型,创建tensor的时候指定

查看tensor的基本属性的操作

x.shape # 矩阵维度
x.size() # 张量维度
x.dim() # 张量维度数目
x.type() # 张量的数据类型
x.dtype # 查看张量的数据类型
x.T # 矩阵转置

查看张量位置

id(x) #查看tensor x引用地址
x.device #查看tensor x位于cpu还是gpu上

2.1.2 运算符

  • 按元素计算,下列常见运算均支持:
    • 常用的+、-、x,/ 均支持按照元素计算
    • 求幂运算xy
    • 一元运算符 比如 torch.exp(x)
    • x == y
      • 当x和y相等的时候,逐元素判断是否相等
    • 聚合操作:sum(), mean(), max(), min()
  • 向量叠加或者连结(concatenate)
    • 多个张量concatenate
    • dim = 0 按照行连接
    • dim = 1 按照列连接
torch.cat((x,y), dim=0) # 按行叠加
torch.cat((x,y), dim=1) # 按列叠加

2.1.3 广播机制

  • 广播机制指的是:当两个向量之间进行操作的时候,由于不满足传统条件,numpy 或者 pytorch会自动进行补全
a = torch.arange(3).reshape((3,1))
b = torch.arange(2).reshape((1,2))
a+b

换算成numpy

a=np.arange(3).reshape((3,1))
b=np.arange(2).reshape((1,2))
>>> a
array([[0],
[1],
[2]])
>>> b
array([[0, 1]])

a进行复制列,b进行复制行。将a和b分别变成长度为 3*2的矩阵后,进行想加

>>> a+b
array([[0, 1],
[1, 2],
[2, 3]])

2.1.4 索引和切片

张量中的元素可以进行索引操作,和python的数组操作一致

  • 0表示第一个元素;-1表示最后一个元素,可以通过指定范围选择某个范围中的数据 ;
  • 特殊符号":" 表示 沿某一个轴的所有元素
  • 可以利用切片操作对选择范围中的数据进行赋值 X[1,2]=9
>>> x=np.arange(9).reshape(3,3)
>>> x
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
>>> x[0,:]
array([0, 1, 2])

2.1.5 原位计算

矩阵操作某些写法会导致变量重新分配内存,即变量对应的内存地址会发生变化。ex: X = X + Y。但是有时候为了节省内存,我们更希望原位操作。针对矩阵,如下是表示原位操作:

  • 更新矩阵:X[:] = X + Y 或者 X += Y
  • *=

函数id(变量) 用来表示变量引用指向的内存空间的地址,可以用来判断两个变量是否是内存上相同。

>>> X=np.zeros(4).reshape(2,2)
>>> X
array([[0., 0.],
[0., 0.]])
>>> Y = np.ones(4).reshape(2,2)
>>> id(X)
140502555798464
>>> X += Y
>>> id(X)
140502555798464

注意:Pytorch代码中很多操作都是原位操作,远不止这种写法

函数中,以_结尾的函数,都是原位操作,会改变tensor本身。

比如:y.add_(x),x.copy_(y),x_t(), x.squeeze_(),x.unsqueeze_(),mul_,reshape_()

In [69]: id(a)
Out[69]: 139970692801520
In [70]: a.add_(a)
Out[70]:
tensor([[ 0., 4., 8.],
[12., 16., 20.]])
In [71]: id(a)
Out[71]: 139970692801520

2.1.6 转换为其他python对象

将pytorch定义的tensor 转换为numpy(底层都一样)

A = X.numpy() # 将tensor转化为numpy
B = torch.tensor(A) # 进行转换
type(A), type(B)

(numpy.ndarray, torch.Tensor)

  • 将常规张量转化为numpy:
a=torch.tensor([2,3])
b=a.numpy()
type(a),type(b)

(torch.Tensor, numpy.ndarray)

  • 将大小为1的张量转换为python标量,使用item 或者 python的内置函数
a = torch.tensor([3.5])
a.item(), float(a), int(a) # 将张量转化为标量

适用于numpy

>>> a=np.array([1.0])
>>> type(a.item())
<type 'float'>
>>> type(int(a))
<type 'int'>

将numpy转化为张量

torch.from_numpy(np.array([1,2,3]))

2.1.7 张量叠加与切割

张量的拼接:几个小张量拼接成一个大的张量。主要的函数有torch.cat,torch.stack

张量的切割:将一个张量切割成几个小的。主要的函数有torch.chunk,torch.split

下面注意介绍:

torch.cat

功能:在给定维度上对张量序列进行叠加操作,所有的张量必须有相同的形状(连接维度除外)或者为空

In [87]: a
Out[87]:
tensor([[0., 1., 2.],
[3., 4., 5.]])
In [88]: torch.cat((a,a),dim=0) # 沿行叠加
Out[88]:
tensor([[0., 1., 2.],
[3., 4., 5.],
[0., 1., 2.],
[3., 4., 5.]])
In [89]: torch.cat((a,a),dim=1) # 沿列叠加
Out[89]:
tensor([[0., 1., 2., 0., 1., 2.],
[3., 4., 5., 3., 4., 5.]])

torch.stack

功能:在一个新的维度上,叠加张量,所有的张量必须是相同的大小

In [92]: a # shape: 3*2
Out[92]:
tensor([[0., 1., 2.],
[3., 4., 5.]])
In [93]: torch.stack((a,a),dim=0) # shape 2 * 2 * 3
Out[93]:
tensor([[[0., 1., 2.],
[3., 4., 5.]],
[[0., 1., 2.],
[3., 4., 5.]]])
In [97]: torch.stack((a,a),dim=1) # shape torch.Size([2, 2, 3])
Out[97]:
tensor([[[0., 1., 2.],
[0., 1., 2.]],
[[3., 4., 5.],
[3., 4., 5.]]])
In [100]: torch.stack((a,a),dim=2) # shape torch.Size([2, 3, 2])
Out[100]:
tensor([[[0., 0.],
[1., 1.],
[2., 2.]],
[[3., 3.],
[4., 4.],
[5., 5.]]])

torch.chunk

用法:torch.chunk(*input*, *chunks*, *dim=0*) → List of Tensors

功能:将一个大的tensor按照不同的维度分割成k个小的tensor。每个数据块都是输入张量的一个视图。

In [4]: a=torch.arange(6).reshape(2,3)
In [5]: a.chunk(3)
Out[5]: (tensor([[0, 1, 2]]), tensor([[3, 4, 5]]))
In [6]: a.chunk(3,dim=0) # 按照维度为0进行分割
Out[6]: (tensor([[0, 1, 2]]), tensor([[3, 4, 5]]))
In [7]: a.chunk(3,dim=1) # 按照维度为1进行分割
Out[7]:
(tensor([[0],
[3]]),
tensor([[1],
[4]]),
tensor([[2],
[5]]))

torch.split

用法:torch.split(*tensor*, *split_size_or_sections*, *dim=0*)

功能:将张量分成数据块,每个数据块都是原始张量的视图。和 torch.chunk功能相同

  • 当split_size_or_sections 为整数的时候,函数会沿着维度方向尽量切割成长度为plit_size_or_sections的tensor
  • 当split_size_or_sections 是list的时候,函数根据list的设置,沿着设定的维度方向,切割成长度不同的tensor
In [13]: a
Out[13]:
tensor([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])
In [16]: torch.split(a,2,dim=0) # 按照行分
Out[16]:
(tensor([[0, 1],
[2, 3]]),
tensor([[4, 5],
[6, 7]]),
tensor([[8, 9]]))
In [17]: torch.split(a,2,dim=1) # 按照列分
Out[17]:
(tensor([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]]),)

2.1.8 维度变化

维度变化函数可以直接对tensor的维度进行更改,比如 torch.flatten, torch.squeeze,torch.unsqueeze 。严格意义上来讲,torch.split,torch.cat 还是传统的 聚合函数如torch.sum 也是维度变化。

torch.flatten:扁平化

用法:torch.flatten(*input*, *start_dim=0*, *end_dim=-1*) → [Tensor]

功能:将张量进行进行压扁操作(扁平化操作)。 如果张量是1维,不需要压扁;如果张量是2维,压扁到1维;如果张量是3维,那么可以根据实际的需求进行全部压扁或者部分压扁操作。

输入:

  • start_dim:从那一维度开始flatten
  • end_dim:从那一维度结束flatten的操作

注意:维度dim从0开始,-1表示最后一维。

案例: 二维压一维

In [115]: a=torch.arange(4).reshape(2,2)
In [116]: a
Out[116]:
tensor([[0, 1],
[2, 3]])
In [117]: torch.flatten(a)
Out[117]: tensor([0, 1, 2, 3])

三维压二维

In [126]: a=torch.arange(12).reshape(2,2,3)
In [127]: a.flatten(0,1) # 将第0维消除 变成 4*3
Out[127]:
tensor([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11]])
In [128]: a.flatten(0,2) # 等价于a.flatten, a.flatten(0,-1)直接压到以维
Out[128]: tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
In [129]: a.flatten(1,2) # 将第1维压了,变成 2 * 6 按照什么方向压的
Out[129]:
tensor([[ 0, 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10, 11]])

torch.squeeze

用法:torch.squeeze(*input*, *dim=None*) → [Tensor]

功能:squeeze 是挤压,消除,去掉的意思

  • 对维度进行压缩,不设置参数,就是去掉矩阵中所有维数为1的维度。比如矩阵(1*3) 去掉维数为1的,大小变为长度为(3) 的tensor
  • 如果设置dim,那么就是去掉指定维为1的操作 如 矩阵(2*1*2), dim=1,那么输出矩阵就是2*2
  • squeeze 和 unsqueeze对矩阵的数据个数都没有损失

案例

In [138]: a=torch.arange(3).reshape(1,3)
In [139]: b=torch.squeeze(a)
In [140]: b,b.shape
Out[140]: (tensor([0, 1, 2]), torch.Size([3]))

去掉指定维度

In [147]: a=torch.arange(6).reshape(2,1,3)
In [148]: b=torch.squeeze(a,dim=1)
In [149]: b.shape
Out[149]: torch.Size([2, 3])

torch.unsequeeze

用法:torch.unsqueeze(*input*, *dim*) → [Tensor]

功能:在指定位置增加维数为1的维度,比如一个长度为3的向量,在dim=0位置增加一个维度,就是(1*3),在dim=1的位置增加一个维度,张量维度就变为(3*1)

In [151]: a=torch.tensor([1,2,3])
In [152]: b=a.unsqueeze(dim=0)
In [153]: c=a.unsqueeze(dim=1)
In [154]: b.shape,c.shape
Out[154]: (torch.Size([1, 3]), torch.Size([3, 1]))

2.2 矩阵乘法介绍

torch中涉及矩阵乘法函数较多:如

  • torch.mul, torch.multiply,*
  • torch.dot, torch.mv,torch.mm,torch.bmm,torch.matmul

矩阵乘法最基本有两种方式:

  • 点乘:按元素相乘。即两个矩阵中相同位置元素的乘积构成了新矩阵相应位置的元素
  • 叉乘:传统矩阵乘法

2.2.1 点乘

*torch.mul,torch.multiply 三者含义相同。

两个张量的点乘注意:

  • 两个张量维度相同 ,按相同位置相同元素相乘
  • 两个张量维度不同的时候,先通过广播转化为相同

向量和向量

In [29]: a=torch.ones(3,dtype=torch.float)
In [30]: b=torch.arange(3,dtype=torch.float)
In [31]: a*b,torch.mul(a,b)
Out[31]: (tensor([0., 1., 2.]), tensor([0., 1., 2.]))
In [32]: a*b == torch.mul(a,b)
Out[32]: tensor([True, True, True])

向量和矩阵

In [33]: a=torch.arange(6,dtype=torch.float).reshape(2,3)
In [34]: b=torch.ones(3,dtype=torch.float)
In [35]: a
Out[35]:
tensor([[0., 1., 2.],
[3., 4., 5.]])
In [36]: b
Out[36]: tensor([1., 1., 1.])
In [37]: torch.mul(a,b)
Out[37]:
tensor([[0., 1., 2.],
[3., 4., 5.]])

矩阵和矩阵

In [41]: a=torch.arange(6,dtype=torch.float).reshape(2,3)
In [42]: a
Out[42]:
tensor([[0., 1., 2.],
[3., 4., 5.]])
In [43]: b=a
In [45]: torch.mul(a,b)
Out[45]:
tensor([[ 0., 1., 4.],
[ 9., 16., 25.]])

2.2.2 叉乘

叉乘就是传统的矩阵方法,两个向量的叉乘即两个向量的内积。

向量和向量 torch.dot

In [3]: a=torch.ones(3,dtype=torch.float)
In [4]: b=torch.arange(3,dtype=torch.float)
In [5]: a
Out[5]: tensor([1., 1., 1.])
In [7]: b
Out[7]: tensor([0., 1., 2.])
In [8]: torch.dot(a,b) # 矩阵点乘
Out[8]: tensor(3.)

向量矩阵 torch.mv

In [11]: a=torch.arange(6,dtype=torch.float).reshape(2,3)
In [12]: b=torch.ones(3,dtype=torch.float)
In [13]: a
Out[13]:
tensor([[0., 1., 2.],
[3., 4., 5.]])
In [14]: b
Out[14]: tensor([1., 1., 1.])
In [15]: torch.mv(a,b)
Out[15]: tensor([ 3., 12.])

矩阵矩阵 torch.mm

In [16]: a=torch.arange(6,dtype=torch.float).reshape(2,3)
In [17]: b=torch.ones(3,2,dtype=torch.float)
In [18]: a
Out[18]:
tensor([[0., 1., 2.],
[3., 4., 5.]])
In [19]: b
Out[19]:
tensor([[1., 1.],
[1., 1.],
[1., 1.]])
In [20]: torch.mm(a,b)
Out[20]:
tensor([[ 3., 3.],
[12., 12.]])

批量矩阵乘法 torch.bmm

bmm 表示批量矩阵乘法。能够进行多个矩阵的点乘。bmm分别表示batch(批量)、matrix(矩阵)、matrix(矩阵)的首个字母

In [22]: a=torch.tensor([[[1,2],[3,4]],[[1,2],[3,4]],[[1,2],[3,4]]],dtype=torch.float)
In [23]: b=torch.tensor([[[3,4],[1,2]],[[3,4],[1,2]],[[3,4],[1,2]]],dtype=torch.float)
In [26]: a.shape,b.shape
Out[26]: (torch.Size([3, 2, 2]), torch.Size([3, 2, 2]))
In [27]: torch.bmm(a,b)
Out[27]:
tensor([[[ 5., 8.],
[13., 20.]],
[[ 5., 8.],
[13., 20.]],
[[ 5., 8.],
[13., 20.]]])

**矩阵乘法统一接口 **torch.matmul

torch.matmul 可以用来计算向量与向量,向量与矩阵,矩阵与矩阵,批量矩阵之间的乘法

2.2.3 torch.einsum

可以包含上述所有操作。

torch.einsum('ij->ji',a) # 矩阵转置
In [48]: torch.einsum('ij->',a) # 矩阵元素整体求和
Out[48]: tensor(15.)
In [49]: torch.einsum('ij->i',a) # 矩阵元素列求和
Out[49]: tensor([ 3., 12.])
In [50]: torch.einsum('ij->j',a) # 行求和
Out[50]: tensor([3., 5., 7.])

矩阵相乘

In [51]: a
Out[51]:
tensor([[0., 1., 2.],
[3., 4., 5.]])
In [52]: b = a
In [53]: torch.einsum('ij,jk->ik',a,b.T) # 矩阵相乘
Out[53]:
tensor([[ 5., 14.],
[14., 50.]])

向量矩阵相乘

In [60]: a
Out[60]:
tensor([[0., 1., 2.],
[3., 4., 5.]])
In [61]: c
Out[61]: tensor([1., 1., 1.])
In [62]: torch.einsum('ij,j->i',a,c)
Out[62]: tensor([ 3., 12.])

向量的点乘和叉乘

注意:向量点乘和叉乘与矩阵点乘和叉乘的区别。
向量的点乘:逐元素想乘,再求和。就是两个向量的内积。

  • 物理含义:两个向量的夹角
  • ab = |a||b|cosθ
x=torch.randn(2)
torch.dot(x,x) # 点乘、内积

向量的叉乘:结果是一个向量

  • 方向满足右手定律。垂直于向量a向量b构成平面的法向量
  • 大小是 两个向量组成的平行四边形的面积 ab = |a||b|cosθ

2.3 数据预处理

2.4 自动微分

深度学习需要求导,通过导数明确优化方向。深度学习的工具都支持自动求导,即自动微分。自动微分的实现依赖于 计算图 和 链式求导规则。通过链式求导法则,可以将求导计算分解为一系列有限的可微分算子,而后通过计算图将整个运算过程描述出来

计算图

计算图是用来描述运算过程的有向无环图。计算图主要有两个元素:节点(node) 和边(edge)。

  • 节点表示数据变量,如向量、矩阵,张量
  • 边表示运算,比如加减乘除torch.mul(),torch.mm()torch.div

叶子节点

叶子借点即leaf_node。叶子节点是用户自己构建的变量,反向传播过程即终点对叶子阶段进行求导。

链式求导法则

2.2 pytorch求导

  1. 创建可以计算梯度的变量
x=torch.tensor([1,2,3],dtype=torch.float32,requires_grad=True)

或者分两步

x=torch.arange(4.0) # 创建tensor
x.requires_grad_(True) #默认情况下 x.grad == None

梯度的默认值是None

  1. 创建函数表达式 & 计算梯度
y = torch.dot(x,x) # 定义表达式
y.backward() # 计算梯度 对y求x的梯度
print(x.grad) # 输出:tensor([0., 2., 4., 6.])
  1. 变量梯度清零

默认情况下,情况下pytorch会累计梯度。如果你要计算以x为自变量的另外一个函数,此时需要先对变量上已经累计的梯度进行清零。

x.grad.zero_() # 梯度清零
y=x.sum() # 以x为自变量构建的另外一个函数
y.backward() # 计算梯度
print(x.grad) # 打印变量x的梯度

3.1 tensor.grad.zero_() 梯度清零的使用场景

  • 构建tensor x。其梯度打来
  • 函数y=f(x) 对y关于x 在某一点求梯度x.grad
  • 使用x 构建函数g=g(x), 如果x不进行梯度清零,那么对g关于x求导,其结果为x.grad = g关于x的导数+y关于x的导数

在深度学习里面,神经网络的参数是在动态变化中,所以其对应的映射关系也是在动态变化中。所以一次反向求导之后,进行第二次反向求导,那么需要将导数清零。

x=torch.tensor([1.0,1.0],requires_grad=True)
y=torch.dot(x,x) * 0.5
g=2*x
y.backward()
g.sum().backward()
x.grad # tensor([3., 3.]) = tensor([1., 1.]) + 2
  1. 分离计算 detach

detach方法:创建一个变量f是变量x的函数,但是 在进行梯度计算的时候,并不对f求x的梯度,将x视作常量。

场景:

y = f(x) * x 对 y求x的导数,此时希望f(x) 当常数,即y对x求导,结果是f(x)

f = x * x
f = f.detach()
y = f * x
y.sum().backward()
print(x.grad)
x.grad == f # 输出: tensor([True, True, True, True])

torch.no_grad()

  • 简介:有一个与detach类似的方法,叫做torch.no_grad(),上下文管理器。在该管理器中,所有的操作都不会计算梯度,但是也只是简单的禁用梯度计算。

  • 机制:在上下文环境中,将所有的tensor的 ``requires_grad 暂时性的设置为False即可

  • 优点

    • 速度提升:因为不需要计算梯度
    • 省内存:不需要存储梯度

注意:

  1. torch.no_grad() 只对当前的执行线程有效,不会影响到其他线程的计算
  2. 一般用于模型的评估(在模型评估阶段不需要计算梯度)
model = ... # Your trained PyTorch model
with torch.no_grad():
inputs = ... # Your evaluation data
outputs = model(inputs)
# Calculate evaluation metrics (accuracy, loss, etc.)

2.3 定义网络

线索条

1、pandas 操作

2、数据类型

3、原位操作

4、构建layer

5、自动微分

  • 自动微分:通过计算机求微分
  • 计算图
    • 动态图
    • 静态图
  • 叶子节点

6、detach、eval 操作是干什么?

番外篇

python可调用对象

python中一切皆对象。函数也是对象,但是不是所有的对象都是函数。即不是所有的对象都可以像函数一样使用。

类的实例对象可以转化为可调用对象。其关键在于类内定义__call__函数。

__call__函数

在类中定义了_call__函数,可以使得类的对象像函数那样被调用。类似于C++类重载() 运算符。

class A:
def __init__(self):
print('This is __init__')
def __call__(self, name):
print('This is call, %s' % name)
a= A() # 输出 This is __init__
a('name') # 输出 This is call, name

判断可调用对象

至少有三种方法可以判断一个对象是否可以调用

  • 使用callable函数,输出true表示该对象可以调用
  • 使用asattr(对象,"__call__")
  • 使用collections模块中的abc中的abc.Callable + isinstance
callable(A) # 输出 True
hasattr(int,"__call__") # 输出 True
isinstance(int,abc.Callable) # s输出 True

常见的可调用对象

  1. 函数(内置、自定义)、数据类型(int)、类中的函数
  2. 实现了__call__方法的类

应用

pytorch基于nn.Module定义的网络实例为什么可以自动调用forward函数?

如上问题案例解析:

class MLP(nn.Module):
def __init__(self):
super(MLP,self).__init__()
self.hidden = nn.Linear(20, 256)
self.out = nn.Linear(256, 10)
def forward(self, x):
hidden_out = F.relu(self.hidden(x))
return self.out(hidden_out)
net = MLP()
x=torch.rand(2, 20)
print(net(x)) # 其会自动调用forward函数

原因:

  • 所有的神经网络都是以nn.Module为基类派生的

  • nn.Module类中定义了 __call__方法,该方法调用了forward函数

  • 定义神经网络必须要定义forwad函数

    综上,实质上利用了python的语言特性。nn.Module是可调用对象 & forward函数会被__init__调用。所以,当执行对象的实例net(x)的时候,底层会自动调用forward方法计算结果

参考

posted @   金字塔下的蜗牛  阅读(92)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示