今天是2025年6月1日,从今天起,我打算新开一个系列,将自己学习《动手学深度学习》的心得和笔记发在掘金上面。
目标是日更,就以这种形式,今天是2.1和2.2,明天就应该是2.3和2.4。我计算过,只有以这种速度更新,才能在两个月内搞定这本书。
本篇文章包括后续的所有文章,都是我在Jupyter Notebook中先自己过了一遍,然后把笔记和代码以markdown的形式放到掘金上。一方面是为了分享,另一方面也是为了督促自己完成。
其实我自己知道真正每天按照计划走很难,达标不容易,但是如果能坚持走下来,相信自己人工智能的实践水平能提升一个档次。
2.1.1 入门
numpy里面的数组只能在CPU上操作,而且不支持自动微分。 深度学习框架(Pytorch, Tensorflow, MXnet)支持GPU计算和自动微分 因此想做深度学习只用numpy是不够的。
ini
import torch
x = torch.arange(12)
x

X = x.reshape(3, 4) 只改变形状,不改变元素的值和数量
既然数量不变,因此reshape以后的数量要和原来对的上,否则会报错
ini
X = x.reshape(3, 4)
X

无论是一维数组还是二维数组,还是其他维数组, 在深度学习框架里面,数组统称为张量Tensor
f"string"这种结构,具体的变量值就放在{}里面, \n放在外面即可
ini
y = torch.arange(22)
Y = y.reshape(2, 11)
print(f"{y} \n\n {Y}")

无论使不使用-1,用reshape的关键都是前后元素的数量要对齐。
ini
Y2 = y.reshape(-1, 11)
Y3 = y.reshape(2, -1)
print(f"{Y2}\n\n{Y3}")

torch.tensor()才是本源 像什么torch.arange(), torch.randn(), torch.zeros(), torch.ones()这些只是对torch.tensor()的快捷操作罢了。
lua
torch.tensor([[1, 2], [3, 4]])

2.1.2 运算符
python
'''
tensor的加减乘除也是得元素数量的对齐
tensor级别的加减乘除本质上就是针对其中相应位置元素的加减乘除。
'''
x = torch.tensor([3.4, 2.1, 5, 6, 7])
y = torch.tensor([1, 2, 3, 4, 5])
ans1, ans2, ans3, ans4, ans5 = x+y, x-y, x*y, x/y, x**y
print(f"{ans1}\n\n{ans2}\n\n{ans3}\n\n{ans4}\n\n{ans5}")

python
'''
事实证明,reshape(3, 4)和reshape((3, 4))效果是一样的。
'''
x = torch.arange(12).reshape(3, 4)
x2 = torch.arange(12).reshape((3, 4))
print(f"{x}\n\n{x2}")

scss
x = torch.arange(12).reshape(3, 4)
y = torch.zeros(3, 4)
ans1, ans2 = torch.cat((x, y), dim = 0), torch.cat((x, y), dim = 1)
print(f"{ans1}\n\n{ans2}")

python
'''
0竖1横
cat是contactnate的缩写,意思是拼接
dim = 0是竖着拼,dim = 1是横着拼
注意x和y要用一个小括号括起来,相当于cat()只用两个参数,一个是(x, y),还有一个是dim
'''
x = torch.arange(12).reshape(3, 4)
y = torch.zeros(3, 1)
ans = torch.cat((x, y), dim = 1)
print(ans)

python
'''
使用torch.cat()的使用只要拼接处对齐就行,没必要要求tensor形状完全一样。
但是使用x == y判断两个tensor中每个位置上的元素是否对应相等,那就形状必须得完全一样了。
'''
x = torch.tensor([1, 2, 3, 4])
y = torch.tensor([1, 1, 3, 3])
x == y

2.1.3 广播机制
lua
import torch
x = torch.tensor([[1, 1, 1], [2, 2, 2]])
y = torch.arange(6).reshape(2, 3)
print(f"{x+y}\n\n{x-y}")

python
'''
广播机制有两种应用场景:
1. 至少有一个维度要对齐
2. 行和列相加
'''
x = torch.arange(15).reshape(3, 5)
y = torch.ones(5)
x+y

2.1.4 索引和切片
python
'''
操作逻辑基本上和传统的数组是一样的。
'''
x = torch.arange(12).reshape(3, 4)
x

python
'''
所以tensor的索引有两种方式,一种是x[1, 2],另一种是x[1][2]
这两种效果是等价的。
'''
x[1, 1] = 100
x

2.1.5 节省内存
ini
x = torch.tensor([[1, 2], [1, 3]])
y = torch.ones(2, 2)
before_y = id(y)
y = x + y
after_y = id(y)
print(f"{before_y} \n\n{after_y}")
before_y == after_y

python
'''
原地更新有两种方法:
1. 切片
2. +=
'''
x = torch.tensor([[1, 2], [1, 3]])
y = torch.ones(2, 2)
before_y = id(y)
y += x
after_y = id(y)
print(f"{before_y} \n\n{after_y}")
before_y == after_y

ini
x = torch.tensor([[1, 2], [1, 3]])
y = torch.ones(2, 2)
before_y = id(y)
y[:, :] = x + y
after_y = id(y)
print(f"{before_y} \n\n{after_y}")
before_y == after_y

2.1.6 转换为其他Python对象
python
path = []
def dfs(self, root, target_node):
path.append(root)
if root.val == target_node.val:
return True
if root.left: dfs(self, root.left, target_node)
if root.right: dfs(self, root.right, target_node)
path.pop()
return False
def dfs(self, root, target_node, path):
if not root:
return False
path.append(root)
if root.val == target_node.val:
return True
if dfs(self, root.left, target_node, path) or dfs(self, root.right, target_node, path):
return True
path.pop()
return False
python
import numpy as np
import torch
'''
list不能直接通过numpy()转换为numpy.ndarray,
但是tensor可以直接通过numpy()转换为numpy.ndarray
如果想把tensor转换为numpy.ndarray,就直接用torch.tensor
'''
X = torch.tensor([1, 2, 3])
X_numpy = X.numpy()
X_torch = torch.tensor(X_numpy)
print(f"{type(X_numpy)}\n\n{type(X_torch)}")

python
'''
int(a)的效果是直接下取整。
只有当tensor中只含有一个元素时,才能直接用int()和float()
'''
a = torch.tensor([3.8])
a, a.item(), float(a), int(a)

2.1 练习
ini
X = torch.arange(20).reshape(4, 5)
Y = torch.ones(4, 5)
print(f"{X>Y}\n\n\n{X<Y}")

问题:广播机制的使用到底可以适用于哪些情况?
回答:从右到左比较:每一对维度之间必须满足以下三个条件中的任意一个
- 相等
- 其中一个为一
- 不存在
python
'''
我大概能体会为什么要从右到左比较了。
因为这三个条件:等;二选一为1;不存在
从右到左和从左到右是不一样的。
更深层次的原因是从里往外,内层必须对的上,外层可以扩展
'''
X = torch.ones(3, 4, 5)
Y = torch.ones(1, 5)
X+Y

上半截是2.1数据操作,下半截是2.2数据预处理
2.2.1 读取数据集
os.path.join()会根据不同的操作系统创建路径。
os.path.join()不会去创建一个文件或文件夹,创建文件夹用os.makedirs(),创建文件用 with open
data的地位应该是和章节并列的
lua
import os
os.makedirs(os.path.join("..", "data"), exist_ok = True)
data_file = os.path.join("..", "data", "house_tiny.csv")
用os.path.join()指定路径就是为了使代码在windows和linux之间通用
lua
with open(data_file, "w") as f:
f.write('NumRooms,Alley,Price\n') # 列名
f.write('NA,Pave,127500\n') # 每行表示一个数据样本
f.write('2,NA,106000\n')
f.write('4,NA,178100\n')
f.write('NA,NA,140000\n')
读csv文件最有效的方法就是pandas里面的read_csv
在用with open()往csv文件里面写东西的时候,第一个写入的就是列名,剩下的就是具体的值。
写入的使用是NA,用pd.read_csv()识别的使用会识别成NaN, NaN代表缺失值
kotlin
import pandas as pd
data = pd.read_csv(data_file)
print(data)

2.2.2 处理缺失值
csv文件被read_csv以后,就成了pandas里面的DataFrame
iloc[slice1, slice2],slice1指定行,slice2再指定列
iloc的参数可以是切片,还可以是具体的数字
如果你用inputs.mean()去填充NaN,那么只会影响数值列,不会影响别的列
单列的dataframe会被视为series,列名在下方;多列的dataframe就是dataframe,列名在上方
scss
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
inputs = inputs.fillna(inputs.mean())
print(inputs)
print()
print(outputs)

pd.get_dummies() 根据值的不同生成不同的列,新值是不同值的有无。
dummy_na = True 的意思是NaN也看作是不同的值中的一个 dummy_na就决定了是否有Alley_nan这一行。 如果dummy_na = True,那么Alley_nan就会单独作为一行。 但是如果dummy_na = False,那么Alley_nan就没有这一行
get_dummies()只会对object类型有用,对数值不起作用
ini
inputs = pd.get_dummies(inputs, dummy_na = True)
print(inputs)

2.2.3 转换为张量格式
数值类型才能转换为张量形式,因此必须先把Object对象,比如字符串用get_dummies改成数值形式再去转换为tensor
pandas里面有一个函数:to_numpy()可以把dataframe转换为numpy numpy又可以进一步通过torch.tensor转换为tensor
python
import torch
X = torch.tensor(inputs.to_numpy(dtype = float))
Y = torch.tensor(outputs.to_numpy(dtype = float))
print(f"{X}\n\n{Y}")

练习
1.创建包含更多行和列的原始数据集
定义csv文件时,直接加后缀.csv就行
往csv文件里面逐行写内容,分隔一定要用英文逗号,中文逗号识别不出来,无法起到分隔效果。
太细节了。一般来说写代码的时候英文逗号后面还会额外空一格,但是在往CSV文件里面写东西的时候就不能空格了 因为 NA, 这种写法会导致NA无法被正确得被识别为NaN,只有前后无空格才能被识别为NaN
lua
import os
import pandas as pd
file_path = os.path.join("..", "data", "2.2数据预处理_练习.csv")
with open(file_path, "w") as f:
f.write("物品,价格,年龄\n")
f.write("iphone10,7999,5\n")
f.write("篮球,NA, 4\n")
f.write("戴尔笔记本,5000,4\n")
f.write("倍思电子笔,NA,NA\n")
data = pd.read_csv(file_path)
data

2. 删除缺失值最多的列
方法一:肉眼看,直接删除
df2 = df1.drop(columns = "balabalabala")通过这种方式可以删除某列
kotlin
data = data.drop(columns = "价格")
data

方法二:写代码自动判断哪一列的NaN是最多的
isna() 缺失了就是True, 没有缺失就是False
ini
is_NaN = data.isna()
NaN_num = is_NaN.sum()
Most_NaN_column = NaN_num.idxmax()
data = data.drop(columns = Most_NaN_column)
data

3. 将预处理后的数据集转换为张量
kotlin
data = data.fillna(data.mean())
data

kotlin
data = pd.get_dummies(data)
data

ini
ts = torch.tensor(data.to_numpy(dtype = int))
ts
