前言

寒假在家无聊,发现自己对人工智能这块儿实在是没有太多的接触,因此找了几天集中入了个门,这里记录一些自己的快速入门路线,主要涉及神经网络这块儿的内容。

一些很好的参考资料

  • 花书,动手学深度学习,链接https://zh-v2.d2l.ai,里面可以下载到代码和ipynb的文件,这里面有个d2l的python包,我这里一直导入错误。然后我直接把d2l源码拿出来放到目录里导入,因为这玩意儿一共也就四个文件,分别对应pytorch、tensorflow等包,我用的pytorch,所以直接把d2l中关于pytorch的一个包拿出来即可使用。

    花书想啃完还是比较困难的,适合日以继日坚持地看,像我这种想速成一下基础的,还是放弃了。感觉比起一个人单独看,也可以看些教学视频一起跟进,效果会比较好,比如李沐这种,b站也都有链接。

  • 吴恩达老师的深度学习视频,b站有很多,我看的是这一版https://www.bilibili.com/video/BV16r4y1Y7jv,配套的笔记在https://github.com/fengdu78/deeplearning_ai_books上,本来是有作业题的,但因为版权问题被取消了(但git历史的链接好像没失效),这一套比较注重基础,如果没啥基础硬啃,也还是比较困难。

  • 《20天吃掉那只Pytorch》《30天吃掉那只TensorFlow2》。这也是很不错的教程,面向深度学习开发工具,每天花上一些时间,日积月累可以有很大成效。

  • 各类机构教学视频,如黑马。这种在b站上诸如“最完整的深度学习”、“x小时学会深度学习”、“不会还有人没看…”等教学视频比比皆是。有些讲的还是可以的,毕竟机构是要赚钱的,内容比较杂,可能需要有针对性地去选择某一讲某一P的内容。

  • github的各种小仓库demo,比如我想找一个pytorch实现的mnist的gan对抗网络,就可以在github上搜索有没有人写过类似的可以借鉴参考。

我的路线

由于博主之前在一个项目里写过数据集相关代码,因此对于Pytorch的Dataset写法比较熟悉,在之前毫无基础的时候也看过一段时间吴恩达老师的教学视频,同时对一些简单的神经网络也知道概念,因此我针对我本身不太了解的内容进行调研和深入。

Pytorch基本流程

我是在朋友的推荐下看的这个视频进行学习【二十分钟搭建神经网络分类Fashion-MNIST数据集时尚物品】,并且有直接可以运行的github代码:https://github.com/TommyZihao/zihaopytorch,通过仔细分析这个简单demo,可以对pytorch的深度学习框架有一个初步的了解。

卷积神经网络

在了解了基本的Linear层后,可以对卷积神经网络进行初步了解,主要是了解nn.Conv2d卷积和nn.MaxPool2d池化的这两个概念,并且要知道图像进入这两个函数后的形态变化,以及在Pytorch中的实现,下面是一个非常简单的demo。

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
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# self.conv1 = nn.Conv2d(1, 6, 5, stride=2) # 可以有跨度,stride表示步长
self.conv1 = nn.Conv2d(1, 6, 5) # 可以有跨度,stride表示步长
self.pool = nn.MaxPool2d((2,2)) # 2x2的池化,下采样,池化核的大小为2x2,也可以有跨度
self.conv2 = nn.Conv2d(6, 16, 3) # 输出通道数为16,卷积核大小为5x5
# channels变厚,高宽变窄
self.fc1 = nn.Linear(16*4*4, 256) # 注意这里的16*4*4需要自行计算后指定
self.fc2 = nn.Linear(256, 10)
self.dropout = nn.Dropout(0.2)

def forward(self, x):
x = F.relu(self.conv1(x)) # 卷积,激活,28-4=24
print(x.shape)
x = self.pool(x) # 池化,24/2=12
print(x.shape)
x = F.relu(self.conv2(x)) # 12-4=8
print(x.shape)
x = self.pool(x) # 8/2=4
print(x.shape) # torch.Size([20, 16, 4, 4])
x = x.view(x.size(0), -1) # 展开
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)
return x

构造数据集

前面使用的都是MNIST数据集,可以直接通过torchvision.datasets.MNIST()函数进行下载,但是我们更得学会自己构造数据集,即去继承torch.utils.data.dataset.Dataset类,并且实现这个类的三个函数(__init____getitem____len__),然后使用 torch.utils.data.DataLoader()构造Loader,在训练时直接迭代即可,下面是一个甲骨文识别的示例数据集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ImageDataset(dataset.Dataset):
def __init__(self):
self.image_size = (93, 81)
self.root = '../dataset/'
self.transform = transforms.Compose([
transforms.Resize(self.image_size), # 图像大小变换
transforms.ToTensor(), # 转换为张量
transforms.Normalize((0.5,), (0.5,)) # 归一化
])
self.datadir = [] # 由于读取的是图片,只存路径,在__getitem__中真正读取
for label in os.listdir(self.root):
path = os.path.join(self.root, label)
for filename in os.listdir(path):
self.datadir.append((os.path.join(path, filename), int(label) - 102)) # 减去数据集的起始标签
def __getitem__(self, index):
path, label = self.datadir[index]
image = self.transform(Image.open(path))
return image, label
def __len__(self):
return len(self.datadir)

练手项目

这里的练手项目指从搭建数据集、构建网络、调整参数训练、保存模型、使用模型的一整套流程。我使用的是一套甲骨文识别的练手项目(其实和手写数字分类差不多),使用卷积层和全连接层,划分了0.9和0.1的训练集和测试集。由于样本比较少,最后只达到了59%的准确率,数据集的写法如上面所示。这里贴一下网络的构造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class OracleRecognize(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(93 * 81, 1024)
self.fc2 = nn.Linear(1024, 512)
self.fc3 = nn.Linear(512, 256)
self.fc4 = nn.Linear(256, 128)
self.fc5 = nn.Linear(128, 40)
self.dropout = nn.Dropout(p=0.005)
def forward(self, x):
x = x.view(x.shape[0], -1)
# 在训练过程中对隐含层神经元的正向推断使用Dropout方法
x = self.dropout(F.relu(self.fc1(x)))
x = self.dropout(F.relu(self.fc2(x)))
x = self.dropout(F.relu(self.fc3(x)))
x = self.dropout(F.relu(self.fc4(x)))
# 在输出单元不需要使用Dropout方法
return F.log_softmax(self.fc5(x), dim=1)

更进一步

在这之后,对最基本的网络已经有了解,接下来我的计划是对目前流行的网络进行学习。如RNN、LSTM、GAN等等,在数据集的使用上尽量采用MNIST这种比较简单的。

这是一个关于logistic、cnn、mlp、rnn、svm、knn等模型的github仓库,里面写了各种网络的MNIST数据集demo,有用的应该就rnn、svm和knn这三个,svm和knn虽然不属于神经网络,但也是人工智能的重要算法之一,可以进行学习。

情感分类

在bert出现之前,我们采用的是分词向量的方式。以IMDB数据集为例,可以从torchtext.datasets中导入IMDB数据集,它是一个二分类的电影评论数据集,对于每句评论,首先将其分词,然后将所有评论的分词结果合并成一个大的集合,即词表。生成词表时可以设置单词的出现频率高于某个阈值才放入。

1
2
3
4
5
6
from torchtext.datasets import IMDB
train_data_iter = IMDB(root='.data', split='train') # Dataset类型的对象
tokenizer = get_tokenizer("basic_english") # 实例化一个分词器
# 将频次小于20的词用"<unk>"代替,将频次高于20的词装入到单词表中Vocab
vocab = build_vocab_from_iterator(yield_tokens(train_data_iter, tokenizer), min_freq=20, specials=["<unk>"])
vocab.set_default_index(0) # 将不在单词表里面的数据索引值设置为0

在处理每个batch时,需要对每个评论进行分词,使用的是同一个分词器,即上述代码中的tokenizer

假设这里batch=64,传进来64*2的列表,即(comment, index) * 64,对每个comment分词后,假设分词数量最多的是max_length,将其将comment维度转换为2维张量,即64 * max_length,其中分词数不满max_length的用0补齐。

分词后得到的是一个个单词,在词表中查找该单词的index替换,最后再传出去。

1
2
3
4
5
6
7
8
9
10
11
12
def collate_fn(batch):
""" 对DataLoader所生成的mini-batch进行后处理 """
target, token_index = [], []
max_length = 0
for i, (label, comment) in enumerate(batch):
tokens = tokenizer(comment) # 进行分词
token_index.append(vocab(tokens))
if len(tokens) > max_length:
max_length = len(tokens)
target.append(label-1) # 标签,将1和2转化为0和1
token_index = [index + [0] * (max_length - len(index)) for index in token_index]
return (torch.tensor(target).to(torch.int64), torch.tensor(token_index).to(torch.int32))

在网络中需要先定义一个nn.Embedding词表,将每个分词转换为向量,向量的长度embed_dim可以自行指定,这会将(batchsize, max_seq_len)扩大一维为(batchsize, max_seq_len, embed_dim),这里列举一个最简单的embeddingbag+DNN。

1
2
3
4
5
6
7
8
9
10
11
class TextClassificationModel(nn.Module):
# 15000须大于词表长度,embed_dim表示每个分词转换为多长的向量,
def __init__(self, vocab_size=15000, embed_dim=64, num_class=2):
super(TextClassificationModel, self).__init__()
# nn.EmbeddingBag词袋是在行与行之间进行求均值
self.embedding = nn.EmbeddingBag(vocab_size, embed_dim, sparse=False) # [bs,embedding_dim]
# 再将得到的均值用全连接层映射到分类数上
self.fc = nn.Linear(embed_dim, num_class)
def forward(self, token_index):
embedded = self.embedding(token_index) # shape: [bs, embedding_dim]
return self.fc(embedded)

GAN对抗网络

GAN包含有两个模型,一个是生成模型(Generative Model),一个是判别模型(Discriminative Model)。生成模型的任务是生成看起来自然真实的、和原始数据相似的实例。判别模型的任务是判断给定的实例看起来是自然真实的还是人为伪造的(真实实例来源于数据集,伪造实例来源于生成模型)。

提出GAN的论文中有这样的一个公式(懒得Latex再打一遍),其中G和D分别是生成模型和判别模型。

GAN公式

一样的,还是用最简单的MNIST数据集体会一下其精髓,这是判别器和生成器的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 输入是长度为 100 的噪声(正态分布随机数)
# 输出是 28x28 的图片
class Generator(nn.Module):
def __init__(self):
super().__init__()
self.main = nn.Sequential(
nn.Linear(100, 256),
nn.ReLU(),
nn.Linear(256, 512),
nn.ReLU(),
nn.Linear(512, 28*28),
nn.Tanh()
)

def forward(self, x): # x 表示长度为100的noise输入
img = self.main(x)
img = img.view(-1, 28, 28, 1)
return img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 输入是 28x28 的图片,输出为二分类的概率值,输出使用sigmoid函数激活0-1
# BCEloss计算交叉熵损失

# 判别器中一般使用nn.LeakyRelu
class Discriminator(nn.Module):
def __init__(self):
super().__init__()
self.main = nn.Sequential(
nn.Linear(28*28, 512),
nn.LeakyReLU(), # 在负值部分使用线性函数,避免梯度消失
nn.Linear(512, 256),
nn.LeakyReLU(),
nn.Linear(256, 1),
nn.Sigmoid()
)

def forward(self, x):
x = x.view(-1, 28*28)
prob = self.main(x)
return prob

这是训练过程的部分代码,后续我会整理完放github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 训练判别器
d_optim.zero_grad() # 梯度清零

real_output = dis(img) # 对于判别器输入真实的图片,real_output是对真实图片的预测结果,接近1
# ones_like:根据给定张量,生成与其形状相同的全1张量或全0张量
d_real_loss = loss_fn(real_output, torch.ones_like(real_output)).to(device) # 计算真实图片的损失
d_real_loss.backward()

gen_img = gen(random_noise) # 生成器生成的图片
# 现在是要优化判别器的参数,因此不需要计算生成器的梯度,使用detach
fake_output = dis(gen_img.detach()) # 对于判别器输入生成的图片,fake_output是对生成图片的预测结果,接近0
d_fake_loss = loss_fn(fake_output, torch.zeros_like(fake_output)) # 计算生成图片的损失
d_fake_loss.backward()

d_loss = d_real_loss + d_fake_loss # 判别器的总损失
d_optim.step() # 更新判别器的参数

# 训练生成器
g_optim.zero_grad() # 梯度清零

fake_output = dis(gen_img) # 对于判别器输入生成的图片,fake_output是对生成图片的预测结果,接近1
g_loss = loss_fn(fake_output, torch.ones_like(fake_output)) # 计算生成图片的损失,希望被判别为1,因此是ones_like
g_loss.backward()
g_optim.step() # 更新生成器的参数

这是练了100次后的对比图,效果并不是很好,因为还需要有很多的技巧来优化如GCGAN和ConditionGAN,我并没有深入研究。

GAN效果对比

强化学习

有待填坑、、

总结

AI领域现在大热,得会一些基础的知识,有机会读研的话继续深造。