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,/ 均支持按照元素计算
- 求幂运算
- 一元运算符 比如
- 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.])
向量的点乘和叉乘
注意:向量点乘和叉乘与矩阵点乘和叉乘的区别。
向量的点乘:逐元素想乘,再求和。就是两个向量的内积。
- 物理含义:两个向量的夹角
- = |a||b|
x=torch.randn(2) torch.dot(x,x) # 点乘、内积
向量的叉乘:结果是一个向量
- 方向满足右手定律。垂直于向量a向量b构成平面的法向量
- 大小是 两个向量组成的平行四边形的面积 = |a||b|
2.3 数据预处理
2.4 自动微分
深度学习需要求导,通过导数明确优化方向。深度学习的工具都支持自动求导,即自动微分。自动微分的实现依赖于 计算图 和 链式求导规则。通过链式求导法则,可以将求导计算分解为一系列有限的可微分算子,而后通过计算图将整个运算过程描述出来
计算图
计算图是用来描述运算过程的有向无环图。计算图主要有两个元素:节点(node) 和边(edge)。
- 节点表示数据变量,如向量、矩阵,张量
- 边表示运算,比如加减乘除
torch.mul()
,torch.mm()
,torch.div
等
叶子节点
叶子借点即leaf_node。叶子节点是用户自己构建的变量,反向传播过程即终点对叶子阶段进行求导。
链式求导法则
2.2 pytorch求导
- 创建可以计算梯度的变量
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
- 创建函数表达式 & 计算梯度
y = torch.dot(x,x) # 定义表达式 y.backward() # 计算梯度 对y求x的梯度 print(x.grad) # 输出:tensor([0., 2., 4., 6.])
- 变量梯度清零
默认情况下,情况下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.grad
- 使用 构建函数, 如果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
- 分离计算 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
-
优点
- 速度提升:因为不需要计算梯度
- 省内存:不需要存储梯度
注意:
torch.no_grad()
只对当前的执行线程有效,不会影响到其他线程的计算- 一般用于模型的评估(在模型评估阶段不需要计算梯度)
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、原位操作
- https://blog.csdn.net/qq_34243930/article/details/106886993
- https://blog.csdn.net/m0_38129460/article/details/90405086
4、构建layer
5、自动微分
- 自动微分:通过计算机求微分
- 计算图
- 动态图
- 静态图
- 叶子节点
6、detach、eval 操作是干什么?
- https://blog.csdn.net/qq_41813454/article/details/135129279
- https://blog.csdn.net/weixin_44584198/article/details/135146573
- https://python-code.dev/articles/324150004
番外篇
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
常见的可调用对象
- 函数(内置、自定义)、数据类型(int)、类中的函数
- 实现了
__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方法计算结果
参考
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)