PyTorch深度学习

学习新知识~,不要害怕边学边查

开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 基础部分
发展趋势与环境配置
Numpy基础操作与基础知识
Tensor变形与数据转换

# 模型训练
Torchvision数据读取与增强
常用函数与网络搭建
损失函数,梯度计算与优化方法
可视化与分布式

# 实战
图像分类
图像分割
文本分类
情感分析
自动摘要

# 目的
基于PyTorch搭建自己的深度学习网络
优化自己的模型与算法

安装使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CPU
# Linux
pip install torch==1.9.0+cpu torchvision==0.10.0+cpu torchaudio==0.9.0 -f https://download.pytorch.org/whl/torch_stable.html
# Mac & Windows
pip install torch torchvision torchaudio

GPU
# Linux & Windows
pip install torch==1.9.0+cu111 torchvision==0.10.0+cu111 torchaudio==0.9.0 -f https://download.pytorch.org/whl/torch_stable.html

验证
import torch
torch.cuda.is_available()

这边推荐使用Visual Studio + Jupyter Notebook

核心数据结构

1
2
3
4
5
6
7
8
9
10
11
12
Numpy数组使用
np.asarray([1])
属性
ndim: 显示数组维度
shape: 显示数组的形状
size: 数组元素总数
dtype: 数组中元素类型

np.ones(): 创建全1的数组
np.zeros(): 创建全0的数组
np.arange(): 创建在[start,end)区间的数组
np.linspace(): 创建开始数值到结束数值的等差数列

深度学习中的常用操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# 数据加载
三类训练数据: 图片,文本以及二维表
注意一个点,视觉任务处理的一般都是RGB格式,如果不是需要进行转换

# Pillow
from PIL import Image
import numpy as np
im = Image.open('test.jpg')
im.size

im_pillow = np.asarray(im)
im_pillow.shape

# OpenCV
import cv2
im_cv2 = cv2.imread('test.jpg')
type(im_cv2)
im_cv2.shape

# 注意
读入图片后,维度为3的原因是图片的格式是RGB(红绿蓝)
Pillow读入后通道的顺序是RGB
OpenCV读入后通道的顺序是BGR

# 获取RGB三个通道的全部数据切片
im_pillow_c1 = im_pillow[:,:,0]
im_pillow_c2 = im_pillow[:,:,1]
im_pillow_c3 = im_pillow[:,:,2]

# 生成全0数组
zeros = np.zeros((im_pillow.shape[0], im_pillow.shape[1], 2))
zeros.shape
输出:(116, 318, 2)

# 将im_pillow_c1切片和zeros进行合并,需要注意数组维度是否一致
im_pillow_c1.shape
zeros.shape

# 第一种方式: 使用newaxis增加一个维度
im_pillow_c1 = im_pillow_c1[:, :, np.newaxis]im_pillow_c1.shape
# 合并数组
im_pillow_c1_3ch = np.concatenate((im_pillow_c1, zeros), axis=2)im_pillow_c1_3ch.shape

# 第二种方式: 直接赋值
im_pillow_c2_3ch = np.zeros(im_pillow.shape)im_pillow_c2_3ch[:,:,1] = im_pillow_c2im_pillow_c3_3ch = np.zeros(im_pillow.shape)im_pillow_c3_3ch[:,:,2] = im_pillow_c3

# 打印RGB通道图像实例
from matplotlib import pyplot as plt
plt.subplot(2, 2, 1)
plt.title('Origin Image')
plt.imshow(im_pillow)
plt.axis('off')
plt.subplot(2, 2, 2)
plt.title('Red Channel')
plt.imshow(im_pillow_c1_3ch.astype(np.uint8))
plt.axis('off')
plt.subplot(2, 2, 3)
plt.title('Green Channel')
plt.imshow(im_pillow_c2_3ch.astype(np.uint8))
plt.axis('off')
plt.subplot(2, 2, 4)
plt.title('Blue Channel')
plt.imshow(im_pillow_c3_3ch.astype(np.uint8))
plt.axis('off')
plt.savefig('./rgb_pillow.png', dpi=150)

# 深拷贝与浅拷贝
深,复制为一个新的数组
浅,只是原有数组的视图,操作会影响原数组
a = np.arange(6)
b = a.view()
b.shape = 2, 3
b[0,0] = 111
a的形状虽然不会改变,但是数值发生变化

# 最大值/最小值对应索引
probs = np.array([0.075, 0.15, 0.075, 0.15, 0.0, 0.05, 0.05, 0.2, 0.25])
np.argmax(probs)

# 数组排序后返回原数组的索引(默认是快速排序)
probs_idx_sort = np.argsort(-probs) #注意,加了负号,是按降序排序
probs_idx_sort
# 取前3的索引
probs_idx_sort[:3]

PyTorch中最基础的计算单元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
Tensor-张量
数据表示
标量(Scalar)
向量(Vector)
矩阵(Matrix)

# 创建Tensor
torch.tensor(data, dtype=None, device=None,requires_grad=False)
data 传入模型的数据
dtype 返回什么类型的Tensor
device 数据返回到的设备,不需要关注,缺省
requires_grad 是否需要在计算中保留对应的梯度信息
torch.from_numpy(ndarry)

# 创建零矩阵Tensor
torch.zeros(*size, dtype=None...)

# 创建单位矩阵Tensor(主对角线元素都为1的矩阵)
torch.eye(size, dtype=None...)

# 创建全1矩阵Tensor
torch.ones(size, dtype=None...)

# 创建随机矩阵Tensor
torch.rand(size) # 生成数据类型为浮点型且维度指定的随机Tensor,0-1区间均匀分布
torch.randn(size) # 生成数据类型为浮点型且维度指定的随机Tensor,均值0,方差1的标准正态分布
torch.normal(mean, std, size) # 生成数据类型为浮点型且维度指定的随机Tensor,可指定均值和标准差
torch.randint(low, high, size)# 生成随机整数的 Tensor,其内部填充的是在[low,high)均匀生成的随机整数

# Tensor转换
Int与Tensor转换
a = torch.tensor(1)
b = a.item()
List与Tensor转换
a = [1, 2, 3]
b = torch.tensor(a)
c = b.numpy().tolist()
NumPy与Tensor转换
torch.Tensor(np)
CPU与GPU的Tensor转换
CPU->GPU: data.cuda()
GPU->CPU: data.cpu()

# Tensor常用操作
获取形状
a = torch.zeros(2, 3, 5)
a.shape
a.size()
统计元素量
a.numel()
矩阵转秩(维度转换)
x = torch.rand(2,3,5)
x.shape
# 写入原有维度的新位置,传递的是索引
x = x.permute(2,1,0)
x.shape
# 只能转换两个维度
x = x.transpose(1,0)
x.shape
# 注意,经过转换之后的矩阵元素是不连续的
形状变换
x = torch.randn(4,4)
x.shape
# 注意,view不能处理元素不连续的Tensor
x = x.view(2,8)
x.shape
# 使用reshape,先将元素排序再进行view操作
x.reshape(4,4)
增减维度
x = torch.rand(2,1,3)
x.shape
# 删除第1维度数据,如果值不为1则返回原始Tensor
y = x.squeeze(1)
y.shape
z = y.squeeze(1)
z.shape
x = torch.rand(2,1,3)
# 在第2维度插入一个维度
y = x.unsqueeze(2)
y.shape

Tensor切分变形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
连接操作
cat
torch.cat(tensors, dim = 0, out = None)
A=torch.ones(3,3)
B=2*torch.ones(3,3)
C=torch.cat((A,B),0) # 按行方向拼接
D=torch.cat((A,B),1) # 按列方向拼接
stack
torch.stack(inputs, dim=0)
A=torch.arange(0,4) # 1维
B=torch.arange(5,9) # 1维
C=torch.stack((A,B),0) # 2维-行方向
D=torch.stack((A,B),1) # 2维-列方向

切分操作
chunk
torch.chunk(input, chunks, dim=0)
# 注意chunks必须是整型
A = torch.tensor([1,2,3,4,5,6,7,8,9,10])
B = torch.chunk(A, 2, 0) # 将A从行方向分为2块

A = torch.ones(4,4) # 2维也是从行方向切分
B = torch.chunk(A, 2, 0)
split
torch.split(tensor, split_size_or_sections, dim=0)
split_size_or_sections为整数按整数切割,为列表时按列表中元素值来切割
A = torch.rand(4,4)
B = torch.split(A, 2, 0) # 行方向
C = torch.split(A, (2,2), 0)
unbind
torch.unbind(input, dim=0)
A = torch.arange(0,16).view(4,4)
B = torch.unbind(A, 0) # 4*4的矩阵按行切片
C = torch.unbind(A, 1) # 4*4的矩阵按列切片

索引操作
index_select
torch.index_select(tensor, dim, index)
A = torch.arange(0,16).view(4,4)
# 4*4矩阵按行方向取1,3索引位置数据
B = torch.index_select(A,0,torch.tensor([1,3]))
# 4*4矩阵按列方向取0,3索引位置数据
C = torch.index_select(A,1,torch.tensor([0,3]))
masked_select
torch.masked_select(input, mask, out=None)
A = torch.rand(5)
B = A>0.3
# 可以知道,选取A中符合B(>0.3)的数据
C = torch.masked_select(A, B)
# 可以简写
A = torch.rand(5)
C = torch.masked_select(A, A>0.3)

Torchvision-数据读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
DataSet
__init__() 构造函数,自定义数据读取方法,以及对数据进行预处理
__len__() 返回数据集大小
__getitem__() 索引数据集中的某一数据

# 实例
import torch
from torch.utils.data import Dataset

class MyDataset(Dataset):
# 构造函数
def __init__(self, data_tensor, target_tensor):
self.data_tensor = data_tensor
self.target_tensor = target_tensor
# 返回数据集大小
def __len__(self):
return self.data_tensor.size(0)
# 返回索引的数据与标签
def __getitem__(self, index):
return self.data_tensor[index], self.target_tensor[index]

# 生成数据
data_tensor = torch.randn(10, 3)
target_tensor = torch.randint(2, (10,)) # 标签是0或1

# 将数据封装成Dataset
my_dataset = MyDataset(data_tensor, target_tensor)

# 查看数据集大小
print('Dataset size:', len(my_dataset))
'''
输出:
Dataset size: 10
'''

# 使用索引调用数据
print('tensor_data[0]: ', my_dataset[0])
'''
输出:
tensor_data[0]: (tensor([ 0.4931, -0.0697, 0.4171]), tensor(0))
'''

DataLoader
dataset Dataset类型,输入的数据集
batch_size Int类型,每个batch有多少样本
shuffle Bool类型,每个epoch开始的时候,是否对数据进行打乱
num_workers Int类型,加载数据的进程数,默认为0,加载入主进程

# 多进程迭代加载数据
# 实例
from torch.utils.data import DataLoader
tensor_dataloader = DataLoader(dataset=my_dataset, # 传入的数据集, 必须参数
batch_size=2, # 输出的batch大小
shuffle=True, # 数据是否打乱
num_workers=0) # 进程数, 0表示只有主进程

# 以循环形式输出
for data, target in tensor_dataloader:
print(data, target)
'''
输出:
tensor([[-0.1781, -1.1019, -0.1507],
[-0.6170, 0.2366, 0.1006]]) tensor([0, 0])
tensor([[ 0.9451, -0.4923, -1.8178],
[-0.4046, -0.5436, -1.7911]]) tensor([0, 0])
tensor([[-0.4561, -1.2480, -0.3051],
[-0.9738, 0.9465, 0.4812]]) tensor([1, 0])
tensor([[ 0.0260, 1.5276, 0.1687],
[ 1.3692, -0.0170, -1.6831]]) tensor([1, 0])
tensor([[ 0.0515, -0.8892, -0.1699],
[ 0.4931, -0.0697, 0.4171]]) tensor([1, 0])
'''

# 输出一个batch
print('One batch tensor data: ', iter(tensor_dataloader).next())
'''
输出:
One batch tensor data: [tensor([[ 0.9451, -0.4923, -1.8178],
[-0.4046, -0.5436, -1.7911]]), tensor([0, 0])]

# 安装Torchvision
conda install torchvision -c pytorch

# Torchvision默认图像加载器是PIL,可以自己安装一个第三方图像处理库Pillow

# 利用Torchvision读取数据
支持的数据集列表说明(https://pytorch.org/vision/stable/datasets.html)
需要提前下载使用,并不是说Torchvision自带

# MNIST数据(手写数字数据集)
下载地址贴一下(http://yann.lecun.com/exdb/mnist/)
train-images-idx3-ubyte.gz 训练集图片
train-labels-idx1-ubyte.gz 训练集标签
t10k-images-idx3-ubyte.gz 测试集图片
t10k-labels-idx1-ubyte.gz 测试集标签
# 实例
# 以MNIST为例
# 对于torchvision.datasets所支持的所有数据集,它都内置了相应的数据集接口.
# 例如MNIST数据集,torchvision.datasets就有一个MNIST的接口
# 接口内封装了从下载,解压缩,读取数据,解析数据等全部过程
# 这些接口的工作方式差不多,都是先从网络上把数据集下载到指定目录
# 然后再用加载器把数据集加载到内存中,最后将加载后的数据集作为对象返回给用户
import torchvision
mnist_dataset = torchvision.datasets.MNIST(root='./data',
train=True,
transform=None,
target_transform=None,
download=True)
# root 指定保存MNIST数据集的位置,downlowd是Flase,则会从目标位置读取数据集
# downlowd 表示是否下载数据集
# train 表示是否加载训练集数据,为True只加载训练数据,为False只加载测试数据集(并不是所有数据集都做了训练集和测试集划分)
# transform 图像预处理操作,数据增强,归一化,旋转或缩放等
# target_transform 对图像标签进行预处理操作

# 数据预览
mnist_dataset_list = list(mnist_dataset)
print(mnist_dataset_list)
# 会返回(PIL.Image.Image,Int)元组列表代表图像和图像标签
# 图像可以打印出来
'''

Torchvision-数据增强

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# 数据读取之后是PIL对象,需要转换为Tensor
torchvision.transforms包含常用的图像操作
数据类型转换
对PIL.Image和Tensor进行变化和变换的组合

# 数据类型转换
transforms.ToTensor()
transforms.ToPILImage(mode=None)
mode代表PIL.Image的模式,为None(默认值),根据输入数据维度进行推断
1: mode根据输入数据的类型确定具体模式
2: mode为'LA'
3: mode为'RGB'
4: mode为'RGBA'

# 实例
from PIL import Image
from torchvision import transforms

img = Image.open('/Users/xz/Downloads/test.jpg')
display(img)
print(type(img)) # PIL.Image.Image是PIL.JpegImagePlugin.JpegImageFile的基类
'''
输出:
<class 'PIL.JpegImagePlugin.JpegImageFile'>
'''

# PIL.Image转换为Tensor
img1 = transforms.ToTensor()(img)
print(type(img1))
'''
输出:
<class 'torch.Tensor'>
'''

# Tensor转换为PIL.Image
img2 = transforms.ToPILImage()(img1) #PIL.Image.Image
print(type(img2))
'''
输出:
<class 'PIL.Image.Image'>
'''

---

# 调整尺寸
torchvision.transforms.Resize(size, interpolation=2)
size 期望输出的尺寸,如果是(h,w)这样的元组,图像输出尺寸与之匹配
如果是Int类型整数,图像较小边将匹配该整数
interpolation 插值算法,int类型,默认值2,表示PIL.Image.BILINEAR

# 实例
from PIL import Image
from torchvision import transforms

# 定义一个Resize操作
resize_img_oper = transforms.Resize((200,200), interpolation=2)

# 原图
orig_img = Image.open('test.jpg')
display(orig_img)

# Resize操作后的图
img = resize_img_oper(orig_img)
display(img)

---

# 裁剪
torchvision.transforms.CenterCrop(size)
# 随机裁剪
torchvision.transforms.RandomCrop(size, padding=None)
# 分别从四角和中心裁剪
torchvision.transforms.FiveCrop(size)

# 实例
from PIL import Image
from torchvision import transforms

# 定义剪裁操作
center_crop_oper = transforms.CenterCrop((60,70))
random_crop_oper = transforms.RandomCrop((80,80))
five_crop_oper = transforms.FiveCrop((60,70))

# 原图
orig_img = Image.open('test.jpg')
display(orig_img)

# 中心剪裁
img1 = center_crop_oper(orig_img)
display(img1)
# 随机剪裁
img2 = random_crop_oper(orig_img)
display(img2)
# 四角和中心剪裁
imgs = five_crop_oper(orig_img)
for img in imgs:
display(img)

---

# 翻转,p代表随机翻转概率值,为1表示必须执行翻转操作
torchvision.transforms.RandomHorizontalFlip(p=0.5)

# 实例
from PIL import Image
from torchvision import transforms

# 定义翻转操作
h_flip_oper = transforms.RandomHorizontalFlip(p=1)
v_flip_oper = transforms.RandomVerticalFlip(p=1)

# 原图
orig_img = Image.open('jk.jpg')
display(orig_img)

# 水平翻转
img1 = h_flip_oper(orig_img)
display(img1)
# 垂直翻转
img2 = v_flip_oper(orig_img)
display(img2)

---

# 只对Tensor的变换
线性变换
标准化
随机擦除
格式转化

# 标准化
每一个数据点减去所在通道的平均值,再除以所在通道的标准差,数学的计算公式如下:
output=(input−mean)/std
torchvision.transforms.Normalize(mean, std, inplace=False)
mean 表示各通道的均值
std 表示各通道的标准差
inplace 表示是否原地操作,默认为否

# 实例
from PIL import Image
from torchvision import transforms

# 定义标准化操作
norm_oper = transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))

# 原图
orig_img = Image.open('test.jpg')
display(orig_img)

# 图像转化为Tensor
img_tensor = transforms.ToTensor()(orig_img)

# 标准化
tensor_norm = norm_oper(img_tensor)

# Tensor转化为图像
img_norm = transforms.ToPILImage()(tensor_norm)
display(img_norm)

---

# 变换组合
torchvision.transforms.Compose(transforms)

# 实例
from PIL import Image
from torchvision import transforms

# 原图
orig_img = Image.open('test.jpg')
display(orig_img)

# 定义组合操作
composed = transforms.Compose([transforms.Resize((200, 200)),
transforms.RandomCrop(80)])

# 组合操作后的图
img = composed(orig_img)
display(img)

---

# 结合Datasets使用
from torchvision import transforms
from torchvision import datasets

# 定义一个transform
my_transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5), (0.5))
])
# 读取MNIST数据集 同时做数据变换
mnist_dataset = datasets.MNIST(root='./data',
train=False,
transform=my_transform,
target_transform=None,
download=True)

# 查看变换后的数据类型
item = mnist_dataset.__getitem__(0)
print(type(item[0]))
'''
输出:
<class 'torch.Tensor'>
'''

Torchvision-附加功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 常见的网络模型
torchvision.models模块中包含了常见网络模型结构的定义
这些网络模型可以解决以下四大类问题:
图像分类
图像分割
物体检测
视频分类

# 实例化一个GoogLeNet网络
# 随机初始化权重,创建GoogLeNet模型(需要进行训练)
import torchvision.models as models
googlenet = models.googlenet()
# 直接导入训练好的模型
import torchvision.models as models
googlenet = models.googlenet(pretrained=True)

# torchvision.models模块可实例化的全部模型
https://pytorch.org/vision/stable/models.html

# 接下来该干什么->模型微调(fine-tuning)
简单来说就是先在一个比较通用,宽泛的数据集上进行大量训练得出了一套参数
然后再使用这套预训练好的网络和参数,在自己的任务和数据集上进行训练
使用经过预训练的模型,要比使用随机初始化的模型训练效果更好,更容易收敛,并且训练速度更快
在小数据集上也能取得比较理想的效果

# 微调代码
import torch
import torchvision.models as models

# 加载预训练模型
googlenet = models.googlenet(pretrained=True)

# 提取分类层的输入参数
fc_in_features = googlenet.fc.in_features
print("fc_in_features:", fc_in_features)

# 查看分类层的输出参数
fc_out_features = googlenet.fc.out_features
print("fc_out_features:", fc_out_features)

# 修改预训练模型的输出分类数(在图像分类原理中会具体介绍torch.nn.Linear)
googlenet.fc = torch.nn.Linear(fc_in_features, 10)
'''
输出:
fc_in_features: 1024
fc_out_features: 1000
'''

---

# make_grid
主要用于展示数据集或模型输出的图像结果
torchvision.utils.make_grid(tensor, nrow=8, padding=2)
tensor 类型是Tensor或列表,如果输入类型是Tensor
其形状应是 (B x C x H x W)
如果输入类型是列表,列表中元素应为相同大小的图片
nrow 表示一行放入的图片数量,默认为8
padding 子图像与子图像之间的边框宽度,默认为2像素

# 实例
import torchvision
from torchvision import datasets
from torchvision import transforms
from torch.utils.data import DataLoader

# 加载MNIST数据集
mnist_dataset = datasets.MNIST(root='./data',
train=False,
transform=transforms.ToTensor(),
target_transform=None,
download=True)
# 取32张图片的tensor
tensor_dataloader = DataLoader(dataset=mnist_dataset,
batch_size=32)
data_iter = iter(tensor_dataloader)
img_tensor, label_tensor = data_iter.next()
print(img_tensor.shape)
'''
输出:torch.Size([32, 1, 28, 28])
'''
# 将32张图片拼接在一个网格中
grid_tensor = torchvision.utils.make_grid(img_tensor, nrow=8, padding=2)
grid_img = transforms.ToPILImage()(grid_tensor)
display(grid_img)

---

# save_img
保存模型输出图片
torchvision.utils.save_image(tensor, fp, **kwargs)
tensor 类型是Tensor或列表,如果输入类型是 Tensor,直接将Tensor保存
如果输入类型是列表,则先调用make_grid函数生成一张图片的Tensor,然后再保存
fp 保存图片的文件名
**kwargs:make_grid函数中的参数

# 实例,将上方32张图片拼接结果图保存
# 输入为一张图片的tensor 直接保存
torchvision.utils.save_image(grid_tensor, 'grid.jpg')

# 输入为List 调用grid_img函数后保存
torchvision.utils.save_image(img_tensor, 'grid2.jpg', nrow=5, padding=2)

卷积

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# 卷积的特点
稀疏连接与平移不变性

# 卷积如何计算
输入一个4*4的特征图,卷积核大小2*2
特征图
4 1 7 5
4 4 2 5
7 7 2 4
1 0 2 4
卷积核
1 0
2 1
卷积核与输入特征按位做乘积计算再求和
4*1 + 0*1 + 2*4 + 4*1 = 16
然后依次滑动计算覆盖的特征元素
卷积上下左右滑动的长度则是步长,用stride表示

# 标准的卷积
输入的特征有m个通道,宽为w,高为h
输出有n个特征图,宽为w′,高为h′
卷积核的大小为k*k
输出特征图的通道数由卷积核的个数决定,所以卷积核的个数为n
输入特征图有m个通道,所以每个卷积核也要有m个通道
所以我们需要n个卷积核,每个卷积核大小为(m,k,k)

---

# Padding
在有多层卷积层的神经网络中,特征图会越来越小
有时为了让特征图变得不是那么小,可以对特征图进行补零操作
这样做主要有两个目的
1.有的时候需要输入与输出的特征图保持一样的大小
2.让输入的特征保留更多的信息
补零的操作就叫padding,padding为1就是补一圈的0
padding
为字符串,为字符串时只能是'valid'和'same'
valid就是没有padding操作
same则是让输出的特征图与输入的特征图获得相同的大小
为tuple时,表示在特征图的行列指定补多少零

---

# PyTorch中的卷积
卷积操作定义在torch.nn模块中
nn.Conv1d,nn.Conv2d,nn.Conv3d
# 创建Conv2d类
# Conv2d类
class torch.nn.Conv2d(in_channels,
out_channels,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups=1,
bias=True,
padding_mode='zeros',
device=None,
dtype=None)
in_channels 是指输入特征图的通道数,数据类型为int
在标准卷积的讲解中in_channels为m
out_channels 是输出特征图的通道数,数据类型为int
在标准卷积的讲解中out_channels为n
kernel_size 是卷积核的大小,数据类型为int或tuple
需要注意的是只给定卷积核的高与宽即可,在标准卷积的讲解中kernel_size为k
stride 为滑动的步长,数据类型为int或tuple,默认是1
在前面的例子中步长都为1
padding 为补零的方式,注意当padding为valid或same时
stride必须为1
对于kernel_size,stride,padding都可以是tuple类型
当为tuple类型时,第一个维度用于height的信息,第二个维度时用于width的信息
bias 是否使用偏移项

# 验证same方式,实例
import torch
import torch.nn as nn

input_feat = torch.tensor([[4, 1, 7, 5], [4, 4, 2, 5], [7, 7, 2, 4], [1, 0, 2, 4]], dtype=torch.float32)
# 这里需要进行插入维度保证维度为(1,1,4,4)
.unsqueeze(0).unsqueeze(0)
print(input_feat)
print(input_feat.shape)

# 输出:
tensor([[4., 1., 7., 5.],
[4., 4., 2., 5.],
[7., 7., 2., 4.],
[1., 0., 2., 4.]])
torch.Size([4, 4])

conv2d = nn.Conv2d(1, 1, (2, 2), stride=1, padding='same', bias=True)
# 默认情况随机初始化参数
print(conv2d.weight)
print(conv2d.bias)
# 输出:
Parameter containing:
tensor([[[[ 0.3235, -0.1593],
[ 0.2548, -0.1363]]]], requires_grad=True)
Parameter containing:
tensor([0.4890], requires_grad=True)

conv2d = nn.Conv2d(1, 1, (2, 2), stride=1, padding='same', bias=False)
# 卷积核要有四个维度(输入通道数,输出通道数,高,宽)
kernels = torch.tensor([[[[1, 0], [2, 1]]]], dtype=torch.float32)
conv2d.weight = nn.Parameter(kernels, requires_grad=False)
print(conv2d.weight)
print(conv2d.bias)
# 输出:
Parameter containing:
tensor([[[[1., 0.],
[2., 1.]]]])
None

output = conv2d(input_feat)

深度可分离卷积

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# VGG,ResNet,SENet,DenseNet,利用复杂的结构精确提取出有用的信息,但是速度慢,参数量大
# 深度可分离卷积,在效果近似相同的情况下,需要计算量更少
Depthwise(DW)
Pointwise(PW)

DW卷积
有m个卷积核的卷积,每个卷积核通道数为1,m个卷积核分别与输入特征图对应的通道数据做卷积运算
DW卷积的输出是有m个通道的特征图

PW卷积
逐点卷积,将DW输出的m个特征图结合在一起考虑,输出一个具有n个通道的特征图

---

# DW实例
import torch
import torch.nn as nn

# 生成一个三通道的5x5特征图
x = torch.rand((3, 5, 5)).unsqueeze(0)
print(x.shape)
# 输出:
torch.Size([1, 3, 5, 5])
# 请注意DW中,输入特征通道数与输出通道数是一样的
in_channels_dw = x.shape[1]
out_channels_dw = x.shape[1]
# 一般来讲DW卷积的kernel size为3
kernel_size = 3
stride = 1
# DW卷积groups参数与输入通道数一样
dw = nn.Conv2d(in_channels_dw, out_channels_dw, kernel_size, stride, groups=in_channels_dw)

# 注意
1.DW中,输入特征通道数与输出通道数一致
2.一般来讲,DW的卷积核为3*3
3.DW卷积的groups参数与输出通道数一致

---

# PW实例
in_channels_pw = out_channels_dw
out_channels_pw = 4
kernel_size_pw = 1
pw = nn.Conv2d(in_channels_pw, out_channels_pw, kernel_size_pw, stride)
out = pw(dw(x))
print(out.shape)

# 空洞卷积
较小的特征图恢复到较大的特征图时,会带来一定的信息损失
空洞卷积既保证有较大的感受野,同时不用缩小特征图
计算方式,与普通卷积一样,只是将卷积核以一定比例拆分开来,用0填充卷积核
这个分开的比例,称为扩张率,也是Conv2d中的dilation参数
dilation参数默认为1,可以为int或者tuple,为tuple时第一位代表行信息,第二位代表列信息

# 感受野
对于原图,不同层的特征图,计算区域不一样,这个区域就是感受野
感受野越大,代表包含的信息更加全面,语义信息更加抽象

损失函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 拟合
减少偏差的过程

# 过拟合
函数拟合所有的样本点

# 欠拟合
函数跟样本点的拟合效果较差,只有一个大致的趋势

# 损失函数
L(x) = (F(x) - f(x))^2
F(x) 任意一点对应的真实函数
f(x) 模型学习拟合出来的函数
L(x) 评价拟合函数表现效果好坏的度量指标
损失函数种类很多

# 代价函数
上面的损失函数只是一个点,如果将整个样本点的拟合误差做平均
AVG((F(x) - f(x))^2)
实际应用中不严格区分两个函数,只需要知道损失函数是单个样本点的误差,代价函数是所有样本点的误差

---

# 常见损失函数
0-1损失函数 模型预测对了,损失函数值为0,错了,值为1,是最简单的0-1损失函数
平方损失函数 也就是上面举例的那个函数,计算实际与预测的误差值,有时会加上1/2,为了约掉平方项系数
手写分类,花卉识别都可以使用这种简单的损失函数
均方差损失函数(L2损失函数) 预测值与目标值的平方和,和平方损失函数差不多只是做了求平均值
平均绝对误差损失函数 预测值与真实值差异的绝对值之和求均值
交叉熵损失函数 利用信息熵计算公式,表示系统的无序程度,越无序熵越大
softmax损失函数 是交叉熵损失函数的一个特例

计算梯度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
前馈网络
输入层->隐藏层->输出层
神经元之间的连线代表权重,说明网络中每个节点的重要程度
数据传播是单向的,无法后退

导数
导函数值,F=3x,F'=3

偏导数
将其他变量置为常量,F=x^2 + y^2,在x方向F'=2x,在y方向F'=2y

梯度
函数所有偏导数构成的向量就叫梯度,梯度向量的方向即为函数值增长最快的方向

链式法则
两个函数组合起来的符合函数,导数等于里面函数代入外函数值的导数,乘以里面函数之导数
1.f(x) = cos(x)
2.g(x) = x^2 - 1
f'(x) = -sin(x),g'(x)=2x
f'(x) = f'(g(x))g'(x) = -sin(x^2-1)2x
模型通过不断减少损失函数值的方式来进行学习
通常使用梯度下降的方式让损失函数最小化
每一次给模型权重进行更新时,都按照梯度的反方向进行

反向传播
前向传播 数据从输入层经过隐藏层,最后输出
计算误差并传播 计算模型输出结果和真实结果之间的误差
并将这种误差通过某种方式(类似回调)反向传播
即从输出层向隐藏层传递并最后到达输入层
迭代 根据误差不断调整模型的参数值,并不断迭代前面两个步骤,知道模型达到结束训练的条件
其中,如何通过某种方式进行反向传播,如何通过误差值调整模型参数值,这两步是最重要的(统称优化方法)

优化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
批量梯度下降法
需要将所有数据计算误差之后整体做平均
随机梯度下降
每计算一个样本之后就更新一次参数
虽然快,但如果存在噪声数据或错误样本,优化方向不会朝着理想方向
小批量梯度下降
每次不用全量数据,也不用一条数据,而是使用一部分数据
固定数量越小,速度越快越容易陷入局部最优
固定数量越大,速度越慢越不容易陷入局部最优

# 模型设置
模型定义
损失函数定义
优化器定义

# 抽象代码实例
import LeNet #假定我们使用的模型叫做LeNet,首先导入模型的定义类
import torch.optim as optim #引入PyTorch自带的可选优化函数
...
net = LeNet() #声明一个LeNet的实例
criterion = nn.CrossEntropyLoss() #声明模型的损失函数,使用的是交叉熵损失函数
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
# 声明优化函数,我们使用的就是之前提到的SGD,优化的参数就是LeNet内部的参数,lr即为之前提到的学习率

#下面开始训练
for epoch in range(30): #设置要在全部数据上训练的次数

for i, data in enumerate(traindata):
#data就是我们获取的一个batch size大小的数据

inputs, labels = data #分别得到输入的数据及其对应的类别结果
# 首先要通过zero_grad()函数把梯度清零,不然PyTorch每次计算梯度会累加,不清零的话第二次算的梯度等于第一次加第二次的
optimizer.zero_grad()
# 获得模型的输出结果,也即是当前模型学到的效果
outputs = net(inputs)
# 获得输出结果和数据真正类别的损失函数
loss = criterion(outputs, labels)
# 算完loss之后进行反向梯度传播,这个过程之后梯度会记录在变量中
loss.backward()
# 用计算的梯度去做优化
optimizer.step()
...

构建网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# 随机生成训练集X和对应的标签集Y
import numpy as np
import random
from matplotlib import pyplot as plt

w = 2
b = 3
xlim = [-10, 10]
x_train = np.random.randint(low=xlim[0], high=xlim[1], size=30)

y_train = [w * x + b + random.randint(0,2) for x in x_train]

plt.plot(x_train, y_train, 'bo')

---

# 构建模型
import torch
from torch import nn

class LinearModel(nn.Module):
def __init__(self):
super().__init__()
self.weight = nn.Parameter(torch.randn(1))
self.bias = nn.Parameter(torch.randn(1))

def forward(self, input):
return (input * self.weight) + self.bias

# 知识点
必须继承nn.Module类
重写__init__()方法 将需要学习的参数放到构造函数中
forward()必须重写 定义模型如何计算输出,前向传播

---

# 模型训练
model = LinearModel()
# 定义优化器
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, weight_decay=1e-2, momentum=0.9)

y_train = torch.tensor(y_train, dtype=torch.float32)
for _ in range(1000):
input = torch.from_numpy(x_train)
output = model(input)
loss = nn.MSELoss()(output, y_train)
model.zero_grad()
loss.backward()
optimizer.step()

# 查看训练1000次后的参数结果
for parameter in model.named_parameters():
print(parameter)
# 输出:
('weight', Parameter containing:
tensor([2.0071], requires_grad=True))
('bias', Parameter containing:
tensor([3.1690], requires_grad=True))

---

# 模型保存
只保存训练好的参数
torch.save(model.state_dict(), './linear_model.pth')
state_dict是一个字典,存储模型可训练的参数
model.state_dict()
输出:OrderedDict([('weight', tensor([[2.0071]])), ('bias', tensor([3.1690]))])

保存网络结构与参数
# 保存整个模型
torch.save(model, './linear_model_with_arc.pth')

---

# 模型加载
加载只保存参数的模型
# 先定义网络结构
linear_model = LinearModel()
# 加载保存的参数
linear_model.load_state_dict(torch.load('./linear_model.pth'))
linear_model.eval()
for parameter in linear_model.named_parameters():
print(parameter)
输出:
('weight', Parameter containing:
tensor([[2.0071]], requires_grad=True))
('bias', Parameter containing:
tensor([3.1690], requires_grad=True))

加载保存了网络结构与参数的模型
# 加载模型,不需要创建网络了
linear_model_2 = torch.load('./linear_model_with_arc.pth')
linear_model_2.eval()
for parameter in linear_model_2.named_parameters():
print(parameter)
# 输出:
('weight', Parameter containing:
tensor([[2.0071]], requires_grad=True))
('bias', Parameter containing:
tensor([3.1690], requires_grad=True))

---

# 微调
from PIL import Image
import torchvision.models as models
import torchvision
import torchvision.transforms as transforms

alexnet = models.alexnet(pretrained=True)
# 柯基狗的图片
im = Image.open('dog.jpg')

transform = transforms.Compose([
transforms.RandomResizedCrop((224,224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

input_tensor = transform(im).unsqueeze(0)
alexnet(input_tensor).argmax()
# 263代表柯基类别标签
输出:263

# 注意点,输入数据需要时3通道的数据

---

# 重新训练的微调
# 打印32*32的训练集图片
cifar10_dataset = torchvision.datasets.CIFAR10(root='./data',
train=False,
transform=transforms.ToTensor(),
target_transform=None,
download=True)
# 取32张图片的tensor
tensor_dataloader = DataLoader(dataset=cifar10_dataset,
batch_size=32)
data_iter = iter(tensor_dataloader)
img_tensor, label_tensor = data_iter.next()
print(img_tensor.shape)
grid_tensor = torchvision.utils.make_grid(img_tensor, nrow=16, padding=2)
grid_img = transforms.ToPILImage()(grid_tensor)
display(grid_img)

# 提取分类层的输入参数
fc_in_features = alexnet.classifier[6].in_features

# 修改预训练模型的输出分类数
alexnet.classifier[6] = torch.nn.Linear(fc_in_features, 10)
print(alexnet)

# 数据读入
transform = transforms.Compose([
transforms.RandomResizedCrop((224,224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
cifar10_dataset = torchvision.datasets.CIFAR10(root='./data',
train=False,
transform=transform,
target_transform=None,
download=True)
dataloader = DataLoader(dataset=cifar10_dataset, # 传入的数据集, 必须参数
batch_size=32, # 输出的batch大小
shuffle=True, # 数据是否打乱
num_workers=2) # 进程数, 0表示只有主进程

# 定义优化器
optimizer = torch.optim.SGD(alexnet.parameters(), lr=1e-4, weight_decay=1e-2, momentum=0.9)

# 开始模型训练
# 训练3个Epoch
for epoch in range(3):
for item in dataloader:
output = alexnet(item[0])
target = item[1]
# 使用交叉熵损失函数
loss = nn.CrossEntropyLoss()(output, target)
print('Epoch {}, Loss {}'.format(epoch + 1 , loss))
#以下代码的含义,我们在之前的文章中已经介绍过了
alexnet.zero_grad()
loss.backward()
optimizer.step()

可视化监控

TensorboardX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
    Tensorboard是TensorFlow的一个附加工具,用于记录训练过程的模型的参数,评价指标与图像等细节内容
并通过Web页面提供查看细节与过程的功能,用浏览器可视化的形式展现
帮助我们在实验时观察神经网络的训练过程,把握训练趋势

# 安装
pip install tensorborad
(pytorch1.8之后自带tensorboradX)
pip install tensorboradX

---

# 使用
torch.utils.tensorboard.writer.SummaryWriter(log_dir=None)
# 记录数字常量
add_scalar(tag, scalar_value, global_step=None, walltime=None)
tag 字符串类型,数据名称,不同名称以不同曲线展示
scalar_value 浮点型,保存的数值
global_step 整数,训练的step数
walltime 浮点型,记录发生的时间,默认time.time()
# 记录图像数据
add_image(tag, img_tensor, global_step=None, walltime=None, dataformats='CHW')
img_tensor PyTorch的Tensor或NumPy的array,图像数据
dataformats 字符串类型,图像数据格式,默认CHW(CHW,HWC,HW),Channel * Height * Width

---

# 实例
from torch.utils.tensorboard import SummaryWriter
# PyTorch 1.8之前的版本请使用:
# from tensorboardX import SummaryWriter
import numpy as np

# 创建一个SummaryWriter的实例
writer = SummaryWriter()

for n_iter in range(100):
writer.add_scalar('Loss/train', np.random.random(), n_iter)
writer.add_scalar('Loss/test', np.random.random(), n_iter)
writer.add_scalar('Accuracy/train', np.random.random(), n_iter)
writer.add_scalar('Accuracy/test', np.random.random(), n_iter)

img = np.zeros((3, 100, 100))
img[0] = np.arange(0, 10000).reshape(100, 100) / 10000
img[1] = 1 - np.arange(0, 10000).reshape(100, 100) / 10000

writer.add_image('my_image', img, 0)
writer.close()

---

# 启动
当前目录下生成runs文件后
tensorboard --logdir=runs
浏览器输入http://127.0.0.1:6006

---

# 训练过程可视化
import random
import numpy as np
import torch
from torch import nn

# 模型定义
class LinearModel(nn.Module):
def __init__(self):
super().__init__()
self.weight = nn.Parameter(torch.randn(1))
self.bias = nn.Parameter(torch.randn(1))

def forward(self, input):
return (input * self.weight) + self.bias

# 数据
w = 2
b = 3
xlim = [-10, 10]
x_train = np.random.randint(low=xlim[0], high=xlim[1], size=30)
y_train = [w * x + b + random.randint(0,2) for x in x_train]

# 加入SummaryWriter实例与add_scalar方法
# Tensorboard
from torch.utils.tensorboard import SummaryWriter

# 训练
model = LinearModel()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, weight_decay=1e-2, momentum=0.9)
y_train = torch.tensor(y_train, dtype=torch.float32).unsqueeze(0)

writer = SummaryWriter()

for n_iter in range(500):
input = torch.from_numpy(x_train)
output = model(input)
loss = nn.MSELoss()(output, y_train)
model.zero_grad()
loss.backward()
optimizer.step()
writer.add_scalar('Loss/train', loss, n_iter)

Visdom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
Visdom是Facebook开源的一个专门用于PyTorch的交互式可视化工具
它为实时数据提供了丰富的可视化种类,可以在浏览器中进行查看
并且可以很容易地与其他人共享可视化结果
帮助我们实时监控在远程服务器上进行的科学实验

# 安装
pip install visdom

---

# 启动
python -m visdom.server
浏览器访问http://127.0.0.1:8097

---

# 实例
from visdom import Visdom
import numpy as np
import time

# 将窗口类实例化
viz = Visdom()
# 创建窗口并初始化
viz.line([0.], [0], win='train_loss', opts=dict(title='train_loss'))

for n_iter in range(10):
# 随机获取loss值
loss = 0.2 * np.random.randn() + 1
# 更新窗口图像
viz.line([loss], [n_iter], win='train_loss', update='append')
time.sleep(0.5)

img = np.zeros((3, 100, 100))
img[0] = np.arange(0, 10000).reshape(100, 100) / 10000
img[1] = 1 - np.arange(0, 10000).reshape(100, 100) / 10000
# 可视化图像
viz.image(img)

---

# 训练可视化监控
步骤
实例化一个窗口
初始化窗口信息
更新监听信息
# Visdom
from visdom import Visdom
import numpy as np

# 训练
model = LinearModel()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, weight_decay=1e-2, momentum=0.9)
y_train = torch.tensor(y_train, dtype=torch.float32).unsqueeze(0)

# 实例化一个窗口
viz = Visdom(port=8097)
# 初始化窗口的信息
viz.line([0.], [0.], win='train_loss', opts=dict(title='train loss'))

for n_iter in range(500):
input = torch.from_numpy(x_train)
output = model(input)
loss = nn.MSELoss()(output, y_train)
model.zero_grad()
loss.backward()
optimizer.step()
# 更新监听的信息
viz.line([loss.item()], [n_iter], win='train_loss', update='append')

分布式训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
了解为什么深度学习需要使用GPU
CPU计算,擅长处理大量if else等分支逻辑操作
GPU擅长矩阵向量计算

怎么进行分布式训练
数据与模型进行分布
通过单机多卡与多机多卡实现分布式训练

---

单机单卡
需要了解总的GPU数量
torch.cuda.is_available()
torch.cuda.device_count()
获取GPU实例
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
迁移数据或模型到GPU
data = torch.ones((3, 3))
print(data.device)
# Get: cpu
# 获得device
device = torch.device("cuda: 0")
# 将data推到gpu上
data_gpu = data.to(device)
print(data_gpu.device)
# Get: cuda:0

---

单机多卡
torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
module 模型
device_ids GPU设备号
output_device 输出结果设备号
使用nvidia-smi命令查看GPU使用情况

实例
class ASimpleNet(nn.Module):
def __init__(self, layers=3):
super(ASimpleNet, self).__init__()
self.linears = nn.ModuleList([nn.Linear(3, 3, bias=False) for i in range(layers)])
def forward(self, x):
print("forward batchsize is: {}".format(x.size()[0]))
x = self.linears(x)
x = torch.relu(x)
return x

batch_size = 16
inputs = torch.randn(batch_size, 3)
labels = torch.randn(batch_size, 3)
inputs, labels = inputs.to(device), labels.to(device)
net = ASimpleNet()
net = nn.DataParallel(net)
net.to(device)
print("CUDA_VISIBLE_DEVICES :{}".format(os.environ["CUDA_VISIBLE_DEVICES"]))

for epoch in range(1):
outputs = net(inputs)

# Get:
# CUDA_VISIBLE_DEVICES : 3, 2, 1, 0
# forward batchsize is: 4
# forward batchsize is: 4
# forward batchsize is: 4
# forward batchsize is: 4
当程序GPU数量为4,而batch size为16,每个GPU上的模型forward函数内部的print数据量都是4个
DataParallel会自动将数据切分加载到相应的GPU

---

多机多卡
单机多卡是DataParallel
多机多卡是DistributedDataParallel
DP是单进程控制多GPU,模型会从主GPU复制到其他GPU上,GPU负载不均衡
DDP是多进程控制多GPU,每个GPU创建一个进程,不会有主GPU,使用分布式数据采样器加载数据,保证数据在各个进程之间没有重叠

DDP训练
group 进程组,默认一个组,即一个world
world_size 全局进程个数
rank 进程序号,进程间通讯,表示优先级,0代表主节点
训练流程
初始化进程组
torch.distributed.init_process_group(backend, init_method=None,, world_size=-1, rank=-1, group_name='')
backend 通信后端,nccl用于GPU,gloo用于CPU
init_method url,用于指定进程初始化方式,默认是env://,表示从环境变量初始化,可以使用TCP的方式或共享文件系统
world_size 执行训练的所有进程数,表示一共有多少节点(机器)
rank 进程编号,也是优先级,表示节点编号
group_name 进程组名称
torch.distributed.init_process_group(backend="nccl")
模型并行化
torch.nn.parallel.DistributedDataParallel(module, device_ids=None, output_device=None, dim=0)
net = torch.nn.parallel.DistributedDataParallel(net)
创建分布式数据采样器
train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset)
data_loader = DataLoader(train_dataset, batch_size=batch_size, sampler=train_sampler)

实例
https://github.com/pytorch/examples/blob/master/imagenet/main.py

图像分类实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
需求
建立一个图像分类模型,提供自动识别图片的功能

感知机
一系列输入,通过模型节点,对应两个输出,分别代表输入为指定图片和其他图片的判断,这还不是一个概率
整个流程就是感知机,中间模型节点就是神经元
如何将一组数值,转换为对应的概率?(Softmax函数,有时也会采用sigmoid函数)
实例
import torch
import torch.nn as nn

# 2个神经元的输出y的数值为
y = torch.randn(2)
print(y)
输出:tensor([0.2370, 1.7276])
m = nn.Softmax(dim=0)
out = m(y)
print(out)
输出:tensor([0.1838, 0.8162])

全连接层
模型节点所处的层级称为全连接层(FC层),用于获取最终输出
实例
x = torch.randint(0, 255, (1, 128*128), dtype=torch.float32)
fc = nn.Linear(128*128, 2)
y = fc(x)
print(y)
输出:tensor([[ 72.1361, -120.3565]], grad_fn=<AddmmBackward>)
# 注意y的shape是(1, 2)
output = nn.Softmax(dim=1)(y)
print(output)
输出:tensor([[1., 0.]], grad_fn=<SoftmaxBackward>)
nn.Linear参数
in_features 输入特征的个数
out_features 输出的特征数
bias 是否需要偏移项,默认为True

模型评估计算
TP 真实类别是指定图片,模型也预测为指定图片
FP 真实类别是其他图片,但模型预测为指定图片
FN 真实类别是指定图片,但模型预测为其他图片
TN 真实类别是其他图片,模型也预测为其他图片
精确率计算方式
precision = TP / (TP + FP)
召回率计算方式
recall = TP / (TP + FN)

图像分割实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
语义分割原理
除了实体就是背景
实例分割原理
实体分为实体1,实体2....,以及背景

---

## 数据准备
分割图像的标记
1.下载Labelme工具
我这里想用命令行安装结果一直报错=-=,最终还是使用了dmg包安装,切换python3.9后OK
2.将需要标记的图片放到文件夹中
3.准备一个label.txt文件,写好需要标记的类别
__ignore__
_background_
cat
4.执行命令自动启动Labelme
labelme --labels labels.txt --nodata
5.点击左侧OpenDir,选中图片文件夹
6.点击左侧CreatePolygons,开始标注,标注完成后需要保存(json)
7.执行代码,转化为Mask
python label2voc.py cats cats_output --label label.txt
其中cats文件夹是标记的图片与json文件
cats_output是最终输出的4个文件夹
JPEGImages(训练原图)
SegmentationClass
SegmentationClassPNG(转换Mask)
SegmentationClassVisualization
label.txt是上面设置的标签信息

## label2voc.py
#!/usr/bin/env python

from __future__ import print_function

import argparse
import glob
import os
import os.path as osp
import sys

import imgviz
import numpy as np

import labelme


def main():
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("input_dir", help="input annotated directory")
parser.add_argument("output_dir", help="output dataset directory")
parser.add_argument("--labels", help="labels file", required=True)
parser.add_argument(
"--noviz", help="no visualization", action="store_true"
)
args = parser.parse_args()

if osp.exists(args.output_dir):
print("Output directory already exists:", args.output_dir)
sys.exit(1)
os.makedirs(args.output_dir)
os.makedirs(osp.join(args.output_dir, "JPEGImages"))
os.makedirs(osp.join(args.output_dir, "SegmentationClass"))
os.makedirs(osp.join(args.output_dir, "SegmentationClassPNG"))
if not args.noviz:
os.makedirs(
osp.join(args.output_dir, "SegmentationClassVisualization")
)
print("Creating dataset:", args.output_dir)

class_names = []
class_name_to_id = {}
for i, line in enumerate(open(args.labels).readlines()):
class_id = i - 1 # starts with -1
class_name = line.strip()
class_name_to_id[class_name] = class_id
if class_id == -1:
assert class_name == "__ignore__"
continue
elif class_id == 0:
assert class_name == "_background_"
class_names.append(class_name)
class_names = tuple(class_names)
print("class_names:", class_names)
out_class_names_file = osp.join(args.output_dir, "class_names.txt")
with open(out_class_names_file, "w") as f:
f.writelines("\n".join(class_names))
print("Saved class_names:", out_class_names_file)

for filename in glob.glob(osp.join(args.input_dir, "*.json")):
print("Generating dataset from:", filename)

label_file = labelme.LabelFile(filename=filename)

base = osp.splitext(osp.basename(filename))[0]
out_img_file = osp.join(args.output_dir, "JPEGImages", base + ".jpg")
out_lbl_file = osp.join(
args.output_dir, "SegmentationClass", base + ".npy"
)
out_png_file = osp.join(
args.output_dir, "SegmentationClassPNG", base + ".png"
)
if not args.noviz:
out_viz_file = osp.join(
args.output_dir,
"SegmentationClassVisualization",
base + ".jpg",
)

with open(out_img_file, "wb") as f:
f.write(label_file.imageData)
img = labelme.utils.img_data_to_arr(label_file.imageData)

lbl, _ = labelme.utils.shapes_to_label(
img_shape=img.shape,
shapes=label_file.shapes,
label_name_to_value=class_name_to_id,
)
labelme.utils.lblsave(out_png_file, lbl)

np.save(out_lbl_file, lbl)

if not args.noviz:
viz = imgviz.label2rgb(
lbl,
imgviz.rgb2gray(img),
font_size=15,
label_names=class_names,
loc="rb",
)
imgviz.io.imsave(out_viz_file, viz)


if __name__ == "__main__":
main()

---

## 数据读取
标注工作完成需要使用PyTorch将数据读入

## dataset.py
import os
import torch
import numpy as np

from torch.utils.data import Dataset
from PIL import Image


class CatSegmentationDataset(Dataset):

# 模型输入是3通道数据
in_channels = 3
# 模型输出是1通道数据
out_channels = 1

def __init__(
self,
images_dir,
image_size=32,
):

print("Reading images...")
# 原图所在的位置
image_root_path = images_dir + os.sep + 'JPEGImages'
# Mask所在的位置
mask_root_path = images_dir + os.sep + 'SegmentationClassPNG'
# 将图片与Mask读入后,分别存在image_slices与mask_slices中
self.image_slices = []
self.mask_slices = []
for im_name in os.listdir(image_root_path):

mask_name = im_name.split('.')[0] + '.png'

image_path = image_root_path + os.sep + im_name
mask_path = mask_root_path + os.sep + mask_name

im = np.asarray(Image.open(image_path).resize((image_size, image_size)))
mask = np.asarray(Image.open(mask_path).resize((image_size, image_size)))
self.image_slices.append(im / 255.)
self.mask_slices.append(mask)

def __len__(self):
return len(self.image_slices)

def __getitem__(self, idx):

image = self.image_slices[idx]
mask = self.mask_slices[idx]

image = image.transpose(2, 0, 1)
mask = mask[np.newaxis, :, :]

image = image.astype(np.float32)
mask = mask.astype(np.float32)

return image, mask

---

## 模型训练(网络结构)
UNet是一个非常实用的网络.它是一个典型的 Encoder-Decoder 类型的分割网络,网络结构非常简单
## unet.py
import torch
import torch.nn as nn

class Block(nn.Module):

def __init__(self, in_channels, features):
super(Block, self).__init__()

self.features = features

self.conv1 = nn.Conv2d(
in_channels=in_channels,
out_channels=features,
kernel_size=3,
padding='same',
)
self.conv2 = nn.Conv2d(
in_channels=features,
out_channels=features,
kernel_size=3,
padding='same',
)

def forward(self, input):
x = self.conv1(input)
x = nn.BatchNorm2d(num_features=self.features)(x)
x = nn.ReLU(inplace=True)(x)
x = self.conv2(x)
x = nn.BatchNorm2d(num_features=self.features)(x)
x = nn.ReLU(inplace=True)(x)

return x

class UNet(nn.Module):

def __init__(self, in_channels=3, out_channels=1, init_features=32):
super(UNet, self).__init__()

features = init_features
self.conv_encoder_1 = Block(in_channels, features)
self.conv_encoder_2 = Block(features, features * 2)
self.conv_encoder_3 = Block(features * 2, features * 4)
self.conv_encoder_4 = Block(features * 4, features * 8)

self.bottleneck = Block(features * 8, features * 16)

self.upconv4 = nn.ConvTranspose2d(
features * 16, features * 8, kernel_size=2, stride=2
)
self.conv_decoder_4 = Block((features * 8) * 2, features * 8)
self.upconv3 = nn.ConvTranspose2d(
features * 8, features * 4, kernel_size=2, stride=2
)
self.conv_decoder_3 = Block((features * 4) * 2, features * 4)
self.upconv2 = nn.ConvTranspose2d(
features * 4, features * 2, kernel_size=2, stride=2
)
self.conv_decoder_2 = Block((features * 2) * 2, features * 2)
self.upconv1 = nn.ConvTranspose2d(
features * 2, features, kernel_size=2, stride=2
)
self.decoder1 = Block(features * 2, features)

self.conv = nn.Conv2d(
in_channels=features, out_channels=out_channels, kernel_size=1
)

def forward(self, x):
conv_encoder_1_1 = self.conv_encoder_1(x)
conv_encoder_1_2 = nn.MaxPool2d(kernel_size=2, stride=2)(conv_encoder_1_1)

conv_encoder_2_1 = self.conv_encoder_2(conv_encoder_1_2)
conv_encoder_2_2 = nn.MaxPool2d(kernel_size=2, stride=2)(conv_encoder_2_1)

conv_encoder_3_1 = self.conv_encoder_3(conv_encoder_2_2)
conv_encoder_3_2 = nn.MaxPool2d(kernel_size=2, stride=2)(conv_encoder_3_1)

conv_encoder_4_1 = self.conv_encoder_4(conv_encoder_3_2)
conv_encoder_4_2 = nn.MaxPool2d(kernel_size=2, stride=2)(conv_encoder_4_1)

bottleneck = self.bottleneck(conv_encoder_4_2)

conv_decoder_4_1 = self.upconv4(bottleneck)
conv_decoder_4_2 = torch.cat((conv_decoder_4_1, conv_encoder_4_1), dim=1)
conv_decoder_4_3 = self.conv_decoder_4(conv_decoder_4_2)

conv_decoder_3_1 = self.upconv3(conv_decoder_4_3)
conv_decoder_3_2 = torch.cat((conv_decoder_3_1, conv_encoder_3_1), dim=1)
conv_decoder_3_3 = self.conv_decoder_3(conv_decoder_3_2)

conv_decoder_2_1 = self.upconv2(conv_decoder_3_3)
conv_decoder_2_2 = torch.cat((conv_decoder_2_1, conv_encoder_2_1), dim=1)
conv_decoder_2_3 = self.conv_decoder_2(conv_decoder_2_2)

conv_decoder_1_1 = self.upconv1(conv_decoder_2_3)
conv_decoder_1_2 = torch.cat((conv_decoder_1_1, conv_encoder_1_1), dim=1)
conv_decoder_1_3 = self.decoder1(conv_decoder_1_2)

return torch.sigmoid(self.conv(conv_decoder_1_3))


## 模型训练(优化方法)
Dice Loss函数
当预测值的 Mask 与 GT 越相似,损失就越小;当预测值的 Mask 与 GT 差异度越大,损失就越大
## loss.py
import torch.nn as nn


class DiceLoss(nn.Module):

def __init__(self):
super(DiceLoss, self).__init__()
self.smooth = 1.0

def forward(self, y_pred, y_true):
assert y_pred.size() == y_true.size()
y_pred = y_pred.contiguous().view(-1)
y_true = y_true.contiguous().view(-1)
intersection = (y_pred * y_true).sum()
dsc = (2. * intersection + self.smooth) / (
y_pred.sum() + y_true.sum() + self.smooth
)
return 1. - dsc

## 模型训练开始
将模型,损失函数和优化方法串起来,看下整体的训练流程
## train.py
import argparse
import json
import os

import numpy as np
import torch
import torch.optim as optim
from torch.utils.data import DataLoader
from tqdm import tqdm

from dataset import CatSegmentationDataset as Dataset
from loss import DiceLoss
from unet import UNet


def main(args):
makedirs(args)
device = torch.device("cpu" if not torch.cuda.is_available() else args.device)

loader_train = data_loaders(args)

unet = UNet(in_channels=Dataset.in_channels, out_channels=Dataset.out_channels)
unet.to(device)

dsc_loss = DiceLoss()

optimizer = optim.Adam(unet.parameters(), lr=args.lr)

#logger = Logger(args.logs)
loss_train = []

step = 0

for epoch in tqdm(range(args.epochs), total=args.epochs):
unet.train()

for i, data in enumerate(loader_train):
step += 1

x, y_true = data
x, y_true = x.to(device), y_true.to(device)

y_pred = unet(x)
optimizer.zero_grad()
loss = dsc_loss(y_pred, y_true)

loss_train.append(loss.item())
loss.backward()
optimizer.step()

if (step + 1) % 10 == 0:
print('Step ', step, 'Loss', np.mean(loss_train))
loss_train = []

torch.save(unet, args.ckpts + '/unet_epoch_{}.pth'.format(epoch))


def data_loaders(args):
dataset_train = Dataset(
images_dir=args.images,
image_size=args.image_size,
)

loader_train = DataLoader(
dataset_train,
batch_size=args.batch_size,
shuffle=True,
num_workers=args.workers,
)

return loader_train


def makedirs(args):
os.makedirs(args.ckpts, exist_ok=True)
os.makedirs(args.logs, exist_ok=True)


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Training U-Net model for segmentation of Cat"
)
parser.add_argument(
"--batch-size",
type=int,
default=16,
help="Batch Size (default: 16)",
)
parser.add_argument(
"--epochs",
type=int,
default=100,
help="Epoch number (default: 100)",
)
parser.add_argument(
"--lr",
type=float,
default=0.0001,
help="Learning rate",
)
parser.add_argument(
"--device",
type=str,
default="cuda:0",
help="Device for training (default: cuda:0)",
)
parser.add_argument(
"--workers",
type=int,
default=4,
help="Workers' count (default: 4)",
)
parser.add_argument(
"--ckpts", type=str, default="./ckpts", help="folder to save weights"
)
parser.add_argument(
"--logs", type=str, default="./logs", help="folder to save logs"
)
parser.add_argument(
"--images", type=str, default="./data", help="root folder with images"
)
parser.add_argument(
"--image-size",
type=int,
default=256,
help="target input image size (default: 256)",
)
args = parser.parse_args()
main(args)

---

## 模型预测
使用训练生成的模型来进行语义分割,看看结果呈现效果
## predict_single.py
import torch
import numpy as np

from PIL import Image

img_size = (256, 256)
# 加载模型
unet = torch.load('./ckpts/unet_epoch_99.pth')

unet.eval()

# 加载并处理输入图片
ori_image = Image.open('./PyTorchLearn/data/cutting_out/JPEGImages/cat.jpg')

im = np.asarray(ori_image.resize(img_size))
im = im / 255.
im = im.transpose(2, 0, 1)
im = im[np.newaxis, :, :]
im = im.astype('float32')
# 模型预测
output = unet(torch.from_numpy(im)).detach().numpy()
# 模型输出转化为Mask图片
output = np.squeeze(output)
output = np.where(output>0.5, 1, 0).astype(np.uint8)
mask = Image.fromarray(output, mode='P')
mask.putpalette([0,0,0, 0,128,0])
mask = mask.resize(ori_image.size)
mask.save('output.png')


image = ori_image.convert('RGBA')
mask = mask.convert('RGBA')
# 合成
image_mask = Image.blend(image, mask, 0.3)
image_mask.save("output_mask.png")

NLP基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
常用的分词工具
jieba,HanLP,THULAC,BAT相应的付费工具

安装jieba
pip install jieba

示例
import jieba
text = "今天的天气真好"
# jieba.cut得到的是generator形式的结果
seg = jieba.cut(text)
print(' '.join(seg))

# 词性标注
import jieba.posseg as posseg
text = "今天的天气真好"
# 形如pair('word, 'pos')的结果
seg = posseg.cut(text)
print([se for se in seg])

---

## 文本表示的方法
One-Hot(独热)表示法
假定所有的文字一共有N个单词(也可以是字符)
将每个单词赋予一个单独的序号id,那么对于任意一个单词
都可以采用一个N位的列表(向量)对其进行表示
[0,1,...,0,0],[0,0,...,1,0]
占用空间太小
Count-Based表示法
利用每一个出现单词的序号ID,以及出现的次数进行统计
{3:1,666:1}
有效压缩空间,语序信息缺失
Word Embedding(词嵌入)表示法

---

## 词频统计
TF-IDF
词频(TF)表示关键字在文本中出现的频率
逆向文件频率(IDF)是由包含该词语的文件的数目除以总文件数目

利用NLTK计算TF-IDF示例
from nltk import word_tokenize
from nltk import TextCollection

sents=['i like jike','i want to eat apple','i like lady gaga']
# 首先进行分词
sents=[word_tokenize(sent) for sent in sents]

# 构建语料库
corpus=TextCollection(sents)

# 计算TF
tf=corpus.tf('one',corpus)

# 计算IDF
idf=corpus.idf('one')

# 计算任意一个单词的TF-IDF
tf_idf=corpus.tf_idf('one',corpus)

---

## 词图模型
构建文本图结构,表示词语网络,对语言进行网络图分析,在图上寻找重要作用的词或短语
TextRank算法(PageRank算法)
jieba.analyse.textrank(sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'), withFlag=False)
sentence: 待处理的文本
topK: 最重要的K个关键词

---

## 主题模型
from gensim import corpora, models
import jieba.posseg as jp
import jieba

input_content = [line.strip() for line open ('input.txt', 'r')]
# 老规矩,先分词
words_list = []
for text in texts:
words = [w.word for w in jp.cut(text)]
words_list.append(words)

# 构建文本统计信息, 遍历所有的文本,为每个不重复的单词分配序列id,同时收集该单词出现的次数
dictionary = corpora.Dictionary(words_list)

# 构建语料,将dictionary转化为一个词袋。
# corpus是一个向量的列表,向量的个数就是文档数。你可以输出看一下它内部的结构是怎样的。
corpus = [dictionary.doc2bow(words) for words in words_list]

# 开始训练LDA模型
lda_model = models.ldamodel.LdaModel(corpus=corpus, num_topics=8, id2word=dictionary, passes=10)

---

## 统计语言模型
马尔可夫假设: 对于文本中的一个词,它出现的概率,很大程度上是由这个单词前面的一个或者几个单词决定的
原理: 计算一句话是自然语言(也就是一个正常句子)的概率

情感分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
## 数据准备
影评数据(IMDB 数据集),影评被分为两类: 正面评价与负面评价
需要训练一个情感分析模型,对影评文本进行分类

Torchtext是一个包含常用的文本处理工具和常见自然语言数据集的工具包
pip install torchtext

# 读取IMDB数据集
import torchtext
train_iter = torchtext.datasets.IMDB(root='./data', split='train')
next(train_iter)
root: 是一个字符串,用于指定你想要读取目标数据集的位置,如果数据集不存在,则会自动下载
split: 是一个字符串或者元组,表示返回的数据集类型,是训练集,测试集或验证集,默认是('train', 'test')

---

## 数据处理
# 创建分词器
tokenizer = torchtext.data.utils.get_tokenizer('basic_english')
print(tokenizer('here is the an example!'))
'''
输出:['here', 'is', 'the', 'an', 'example', '!']
'''

# 构建词汇表
def yield_tokens(data_iter):
for _, text in data_iter:
yield tokenizer(text)

vocab = torchtext.vocab.build_vocab_from_iterator(yield_tokens(train_iter), specials=["<pad>", "<unk>"])
vocab.set_default_index(vocab["<unk>"])

print(vocab(tokenizer('here is the an example <pad> <pad>')))
'''
输出:[131, 9, 40, 464, 0, 0]
'''

# 具体示例
# 数据处理pipelines
text_pipeline = lambda x: vocab(tokenizer(x))
label_pipeline = lambda x: 1 if x == 'pos' else 0

print(text_pipeline('here is the an example'))
'''
输出:[131, 9, 40, 464, 0, 0 , ... , 0]
'''
print(label_pipeline('neg'))
'''
输出:0
'''

---

## 生成训练数据
1.利用 torchtext 读取 IMDB 的训练数据集,得到训练数据迭代器
2.使用 to_map_style_dataset 函数将迭代器转化为 Dataset 类型
3.使用 random_split 函数对 Dataset 进行划分,其中 95% 作为训练集,5% 作为验证集
4.生成训练集的 DataLoader
5.生成验证集的 DataLoader

# 生成训练数据
import torch
import torchtext
from torch.utils.data import DataLoader
from torch.utils.data.dataset import random_split
from torchtext.data.functional import to_map_style_dataset

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

def collate_batch(batch):
max_length = 256
pad = text_pipeline('<pad>')
label_list, text_list, length_list = [], [], []
for (_label, _text) in batch:
label_list.append(label_pipeline(_label))
processed_text = text_pipeline(_text)[:max_length]
length_list.append(len(processed_text))
text_list.append((processed_text+pad*max_length)[:max_length])
label_list = torch.tensor(label_list, dtype=torch.int64)
text_list = torch.tensor(text_list, dtype=torch.int64)
length_list = torch.tensor(length_list, dtype=torch.int64)
return label_list.to(device), text_list.to(device), length_list.to(device)

train_iter = torchtext.datasets.IMDB(root='./data', split='train')
train_dataset = to_map_style_dataset(train_iter)
num_train = int(len(train_dataset) * 0.95)
split_train_, split_valid_ = random_split(train_dataset,
[num_train, len(train_dataset) - num_train])
train_dataloader = DataLoader(split_train_, batch_size=8, shuffle=True, collate_fn=collate_batch)
valid_dataloader = DataLoader(split_valid_, batch_size=8, shuffle=False, collate_fn=collate_batch)

---

## 模型构建
# 定义模型
class LSTM(torch.nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional,
dropout_rate, pad_index=0):
super().__init__()
self.embedding = torch.nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_index)
self.lstm = torch.nn.LSTM(embedding_dim, hidden_dim, n_layers, bidirectional=bidirectional,
dropout=dropout_rate, batch_first=True)
self.fc = torch.nn.Linear(hidden_dim * 2 if bidirectional else hidden_dim, output_dim)
self.dropout = torch.nn.Dropout(dropout_rate)

def forward(self, ids, length):
embedded = self.dropout(self.embedding(ids))
packed_embedded = torch.nn.utils.rnn.pack_padded_sequence(embedded, length, batch_first=True,
enforce_sorted=False)
packed_output, (hidden, cell) = self.lstm(packed_embedded)
output, output_length = torch.nn.utils.rnn.pad_packed_sequence(packed_output)
if self.lstm.bidirectional:
hidden = self.dropout(torch.cat([hidden[-1], hidden[-2]], dim=-1))
else:
hidden = self.dropout(hidden[-1])
prediction = self.fc(hidden)
return prediction

---

## 模型训练与评估
# 实例化模型
vocab_size = len(vocab)
embedding_dim = 300
hidden_dim = 300
output_dim = 2
n_layers = 2
bidirectional = True
dropout_rate = 0.5

model = LSTM(vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, bidirectional, dropout_rate)
model = model.to(device)

# 定义损失函数与优化方法
# 损失函数与优化方法
lr = 5e-4
criterion = torch.nn.CrossEntropyLoss()
criterion = criterion.to(device)

optimizer = torch.optim.Adam(model.parameters(), lr=lr)

# 计算loss
import tqdm
import sys
import numpy as np

def train(dataloader, model, criterion, optimizer, device):
model.train()
epoch_losses = []
epoch_accs = []
for batch in tqdm.tqdm(dataloader, desc='training...', file=sys.stdout):
(label, ids, length) = batch
label = label.to(device)
ids = ids.to(device)
length = length.to(device)
prediction = model(ids, length)
loss = criterion(prediction, label) # loss计算
accuracy = get_accuracy(prediction, label)
# 梯度更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_losses.append(loss.item())
epoch_accs.append(accuracy.item())
return epoch_losses, epoch_accs

def evaluate(dataloader, model, criterion, device):
model.eval()
epoch_losses = []
epoch_accs = []
with torch.no_grad():
for batch in tqdm.tqdm(dataloader, desc='evaluating...', file=sys.stdout):
(label, ids, length) = batch
label = label.to(device)
ids = ids.to(device)
length = length.to(device)
prediction = model(ids, length)
loss = criterion(prediction, label) # loss计算
accuracy = get_accuracy(prediction, label)
epoch_losses.append(loss.item())
epoch_accs.append(accuracy.item())
return epoch_losses, epoch_accs

# 计算ACC
def get_accuracy(prediction, label):
batch_size, _ = prediction.shape
predicted_classes = prediction.argmax(dim=-1)
correct_predictions = predicted_classes.eq(label).sum()
accuracy = correct_predictions / batch_size
return accuracy

# 训练过程
n_epochs = 10
best_valid_loss = float('inf')

train_losses = []
train_accs = []
valid_losses = []
valid_accs = []

for epoch in range(n_epochs):
train_loss, train_acc = train(train_dataloader, model, criterion, optimizer, device)
valid_loss, valid_acc = evaluate(valid_dataloader, model, criterion, device)
train_losses.extend(train_loss)
train_accs.extend(train_acc)
valid_losses.extend(valid_loss)
valid_accs.extend(valid_acc)
epoch_train_loss = np.mean(train_loss)
epoch_train_acc = np.mean(train_acc)
epoch_valid_loss = np.mean(valid_loss)
epoch_valid_acc = np.mean(valid_acc)
if epoch_valid_loss < best_valid_loss:
best_valid_loss = epoch_valid_loss
torch.save(model.state_dict(), 'lstm.pt')
print(f'epoch: {epoch+1}')
print(f'train_loss: {epoch_train_loss:.3f}, train_acc: {epoch_train_acc:.3f}')
print(f'valid_loss: {epoch_valid_loss:.3f}, valid_acc: {epoch_valid_acc:.3f}')