上文我们介绍了卷积神经网络极其变形 L S T M LSTM LSTM, G R U GRU GRU 的原理,我们来讲讲用法。循环神经网络主要使用在自然语言处理方面,自然语言处理主要使用的是语言信息,由字和词组成,所以我们一般需要将这些字或者词转为向量,这就不得不提到一个概念:词嵌入,即词向量。
词嵌入
在最初做自然语言处理时,采用
o
n
e
−
h
o
t
one-hot
one−hot 编码来表示一个词,例如一个语料:我喜欢刘德华的歌,分词之后可表示为:我/喜欢/刘德华/的/歌。这里我们有 5 个词,那么使用
o
n
e
−
h
o
t
one-hot
one−hot 编码表示每个词:
我:
[
1
,
0
,
0
,
0
,
0
]
[1, 0, 0, 0, 0]
[1,0,0,0,0]
喜欢:
[
0
,
1
,
0
,
0
,
0
]
[0, 1, 0, 0, 0]
[0,1,0,0,0]
刘德华:
[
0
,
0
,
1
,
0
,
0
]
[0, 0, 1, 0, 0]
[0,0,1,0,0]
的:
[
0
,
0
,
0
,
1
,
0
]
[0, 0, 0, 1, 0]
[0,0,0,1,0]
歌:
[
0
,
0
,
0
,
0
,
1
]
[0, 0, 0, 0, 1]
[0,0,0,0,1]
这种表示方法简单易懂,但是缺点也很明显,无法表示词与词之间的关系,而且如果语料足够大的话,那么维度就会非常大,给服务器带来很大的负载,同时这么多维度中,却只有一维是可用信息,就会造成很大的维度浪费情况。
因为
o
n
e
−
h
o
t
one-hot
one−hot 的这种缺点,深度学习一般采用词嵌入的方式,词嵌入就是将一个全部词语数量为维度的高维空间嵌入到一个低维的连续空间中,词嵌入的具体解释可以看看 自然语言处理与词嵌入,我自信我不能比这位博主写的更加通俗易懂,所以这里大家就只能看看这位博主的大作了。
在
p
y
t
o
r
c
h
pytorch
pytorch 中,词嵌入是通过函数
n
n
.
E
m
b
e
d
d
i
n
g
(
m
,
n
)
nn.Embedding(m, n)
nn.Embedding(m,n) 来实现的,其中
m
m
m 表示所有的单词数目,
n
n
n 表示词嵌入的维度,对应为一个
[
m
,
n
]
[m, n]
[m,n] 的矩阵,例如果有两个词
h
e
l
l
o
,
w
o
r
l
d
hello, world
hello,world,都为五维。对应的矩阵就为
[
2
,
5
]
[2, 5]
[2,5],
E
m
b
e
d
d
i
n
g
Embedding
Embedding 的输入为
[
b
a
t
c
h
_
s
i
z
e
,
m
]
[batch\_size, m]
[batch_size,m],输出为
[
b
a
t
c
h
_
s
i
z
e
,
m
,
n
]
[batch\_size, m, n]
[batch_size,m,n]:
embedding = nn.Embedding(10, 5) # 10个词,每个词为5维
inputs = torch.arange(0, 6).view(3, 2).long() # 输入三行两列,即三个句子,每个句子有两个词
inputs = Variable(inputs)
print("输入大小: ", inputs.shape)
outputs = embedding(inputs)
print("输出大小: ", outputs.shape) # 输出大小
得到的输出如下:
输入大小: torch.Size([3, 2])
输出大小: torch.Size([3, 2, 5])
AI起名
网上做 A I AI AI 写诗的有很多,基本都是参考陈云老师的《深度学习框架 pytorch入门与实践》,我也根据内容,利用爬取的 92251 92251 92251 首诗,完成了此 d e m o demo demo,但是网上同类型太多,我就不写了,这里我们换种方式,爬取一批姓名来完成 A I AI AI 起名的功能,本次利用爬虫获取 985010 985010 985010 组男孩子的名字,基本囊扩了所有的姓氏。
数据处理
在前面了解到,深度学习的数据需要使用相同维度,而名字无法进行分词,为了方便,直接将每个名字当做一个词看待,然后进行处理而考虑到复姓的关系,名字最长可能为 4 4 4,但是为了方便或者出错,在名字后面加上一个终止符 < E N D > <END> <END>,最终组成一个 5 5 5 维向量。
import numpy as np
with open(u"./data/boys.txt", "r", encoding="utf-8") as f: # 获取所有名字
names = []
for line in f:
names.append(line.strip())
data = []
for name in names:
people = list(name) + ["<END>"] # 在名字后面加上终止符并转为列表
if len(people) < 5: # 如果长度小于5,补齐5位
for i in range(5-len(people)):
people.insert(i, "</s>")
else: # 如果长度大于5,截取五位
people = people[: 5]
data.append(np.array(people))
可以看看截取之后,出现的信息:
data[0]

每一个名字为 5 5 5 维,每维为一个汉字,但是代码是无法识别汉字的,所以我们需要想方法将其转为代码能够处理的数字。
words += [word for poetry in data for word in poetry] # 获取所有汉字
words = set(words ) # 去重
word2ix = dict((c, i) for i, c in enumerate(words)) # 字-index
ix2word = dict((i, c) for i, c in enumerate(words)) # index-字
将数字与汉字进行映射之后,我们需要将所有词即名字转为词嵌入的模式:
name_data = [] # 姓名对应的下标向量化
for name in data:
name_txt = [word2ix[ii] for ii in name]
name_data.append(name_txt)
name_data = np.array(name_data)
name_txt = [ix2word[ii] for ii in name_data[0]] # 查看第一个名字
print("".join(name_txt))
输出结果如下所示:

数据处理完成后,将其封装为 t e n s o r tensor tensor:
name_data = torch.from_numpy(name_data)
dataloader = data.DataLoader(name_data, batch_size=64, shuffle=True, num_workers=1)
模型构建
class NameModel(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim): # (所有汉字数,词向量维度,隐藏层维度)
super(NameModel, self).__init__()
self.hidden_dim = hidden_dim # 隐藏层
self.embeddings = nn.Embedding(vocab_size, embedding_dim) # 词表大小/词维度
self.lstm = nn.LSTM(embedding_dim, self.hidden_dim, num_layers=2, batch_first=True) # [输入维度,输出维度,网络层数]/LSTM
self.linear = nn.Linear(self.hidden_dim, vocab_size) # 全连接层
def forward(self, input_, hidden=None): # input_形状 (seq_len, batch_size)
batch_size, seq_len = input_.size()
if hidden is None:
h_0, c_0 = Variable(torch.zeros(2, batch_size, self.hidden_dim)), Variable(
torch.zeros(2, batch_size, self.hidden_dim))
else:
h_0, c_0 = hidden
embeds = self.embeddings(input_)
output, hidden = self.lstm(embeds, (h_0, c_0))
output = self.linear(output.view(seq_len * batch_size, -1))
return output, hidden
输入的字和词通过 n n . E m b e d d i n g nn.Embedding nn.Embedding 得到相应的词向量,然后利用两层 l s t m lstm lstm 提取到词的隐藏信息,再利用隐藏信息将词分类,得到下一个可能出现的词。同时使用相同方法定义损失函数与优化函数:
model = NameModel(len(word2ix), 6, 32) # 字库长度,词维度,隐藏层大小
optimizer = torch.optim.Adam(model.parameters(), 1e-3) # 优化器
criterion = nn.CrossEntropyLoss() # 损失函数
同样可以使用 m o d e l . p a r a m e t e r s model.parameters model.parameters 看看模型的参数:

模型训练
训练方法与前面也是相同,不多讲,直接上代码。
for epoch in range(20):
epoch_loss = 0
for i, data in enumerate(dataloader):
data = data.long()
optimizer.zero_grad()
input_, target = Variable(data[:-1, :]), Variable(data[1:, :]) # 将输入和目标错开
output, _ = model(input_)
loss = criterion(output, target.view(-1))
loss.backward()
optimizer.step()
epoch_loss += loss.data
print('epoch: {}, name_loss: {:.6f}'.format(epoch, epoch_loss/len(dataloader)))
torch.save(model.state_dict(), '%s_%s.pth' % ("./data/name/name", epoch))
上面将输入和目标错开的原因是,正常情况下,比如第一个名字:“钱煜睿“,我们输入姓:“钱”,下一个正确的字为:“煜”,而“煜”就为姓“钱”的目标值,所以我们直接将同一个名字错开一维即可。因为电脑性能的原因,我这里只是将隐藏层大小设置为 32 32 32,使用了 2 2 2 层网络,训练 20 20 20 次,依然跑了一天一夜,各位童鞋如果自己也想练习此 d e m o demo demo,可根据具体情况设置这些参数与 G P U GPU GPU 加速,我这里模型的损失还是有点大,跟训练次数有一定关系,我这里没有计算准确率,后面补上:

生成姓名
def create_name(model, start_words, ix2word, word2ix):
results = list(start_words) # 姓
start_word_len = len(start_words) # 起始长度
inputs = Variable(torch.Tensor([word2ix[results[0]]]).view(1, 1).long()) # 获取第一个词即姓氏
hidden = None
for i in range(200):
output, hidden = model(inputs, hidden)
if i < start_word_len:
w = start_words[i]
inputs = Variable(inputs.data.new([word2ix[w]])).view(1, 1) # 根据输入的姓预测下一个字
else:
top_index = output.data[0].topk(1)[1][0].item() # 取出预测中最可能输出的字
w = ix2word[top_index]
results.append(w)
inputs = Variable(inputs.data.new([top_index])).view(1, 1) # 将输出作为下一次的输入
if w == "<END>":
break
return results[: -1]
first_name = "李"
result = create_name(model, first_name, ix2word, word2ix)
print(''.join(result))
可以看看我们通过模型生成的名字:

多生成几次,看看效果:

总结
其实神经网络的模型创建和训练都是大同小异,不同的是在自己数据处理与参数的调节,而
A
I
AI
AI 起名还有很多可以补充的,比如说根据性别、生肖、姓名长度来就生成姓名,后续我会慢慢补充。
参考
- 陈云:《深度学习框架 pytorch入门与实践》
- 廖星宇:《深度学习之pytorch》