• 快来生成你专属的英文名吧(使用字符级RNN)!


    目录

    一.前言

    二.准备数据

    三.构造神经网络

    四.训练

    五.网络采样(预测)


    一.前言

    数据集为18个国家的姓氏,任务是根据训练得到的模型,在给定国家类别和首字母后,能得到一个与该国人名非常相似的一个人名。

    1. > python sample.py Russian RUS
    2. Rovakov
    3. Uantov
    4. Shavakov
    5. > python sample.py German GER
    6. Gerren
    7. Ereng
    8. Rosher
    9. > python sample.py Spanish SPA
    10. Salla
    11. Parer
    12. Allan
    13. > python sample.py Chinese CHI
    14. Chan
    15. Hang
    16. Iun

    我们使用只有几层线性层的小型RNN。最大的区别在于,这里是输入一个类别之后在每一时刻 输出一个字母。循环预测字符以形成语言通常也被称为“语言模型”。(也可以将字符换成单词或更高级的结构进行这一过程)

    二.准备数据

     测试os函数功能:

    1. import os
    2. filename="data/names\Arabic.txt"
    3. #用于获取指定路径中的基本名称。此方法在内部使用os.path.split()方法将指定路径分为一对(头,尾)。
    4. # os.path.basename()方法将指定的路径拆分为后返回尾部(头,尾)对。
    5. print(os.path.basename(filename))
    6. #将路径的文件名和后缀名分割。其中文件名只是名称。
    7. print(os.path.splitext(os.path.basename(filename)))
    8. print(os.path.splitext(os.path.basename(filename))[0])

    输出:

    1. Arabic.txt
    2. ('Arabic', '.txt')
    3. Arabic

    ***************************************************************************************************

    数据预处理代码:

    点击这里下载数据并将其解压到当前文件夹。

    有一些纯文本文件data/names/[Language].txt,它们的每行都有一个名字。 我们按行将文本按行分割得到一个数组,将Unicode编码转化为ASCII编码,最终得到{language: [names ...]}格式存储的字典变量。

    dataPreprocessing.py:

    1. from __future__ import unicode_literals, print_function, division
    2. from io import open
    3. import glob
    4. import os
    5. import unicodedata
    6. import string
    7. class DataPreprocessing:
    8. def __init__(self):
    9. self.all_letters = string.ascii_letters + " .,;'-" # 注意还有空格
    10. print('string.ascii_letters:', string.ascii_letters) # 大小写的26个字母
    11. print('all_letters:', self.all_letters)
    12. self.n_letters = len(self.all_letters) + 1 # Plus EOS marker
    13. print('总的字符数量:', self.n_letters)
    14. def findFiles(self,path):
    15. # glob.glob返回符合匹配条件的所有文件的路径,即路径中可以用正则表达式
    16. return glob.glob(path)
    17. # 将Unicode字符串转换为纯ASCII, 感谢https://stackoverflow.com/a/518232/2809427
    18. def unicodeToAscii(self,s):
    19. return ''.join(
    20. c for c in unicodedata.normalize('NFD', s)
    21. if unicodedata.category(c) != 'Mn'
    22. and c in self.all_letters
    23. )
    24. # 读取文件并分成几行
    25. def readLines(self,filename):
    26. # strip()返回删除前导和尾随空格的字符串副本
    27. lines = open(filename, encoding='utf-8').read().strip().split('\n')
    28. return [self.unicodeToAscii(line) for line in lines]
    29. def processing(self):
    30. # 构建category_lines字典,列表中的每行是一个类别
    31. category_lines = {}
    32. all_categories = []
    33. for filename in self.findFiles('data/names/*.txt'):
    34. # print(filename) filename是一个路径
    35. category = os.path.splitext(os.path.basename(filename))[0]
    36. all_categories.append(category)
    37. lines = self.readLines(filename)
    38. category_lines[category] = lines
    39. n_categories = len(all_categories)
    40. if n_categories == 0:
    41. raise RuntimeError('Data not found. Make sure that you downloaded data '
    42. 'from https://download.pytorch.org/tutorial/data.zip and extract it to '
    43. 'the current directory.')
    44. return category_lines,all_categories,n_categories,self.all_letters,self.n_letters;
    45. data=DataPreprocessing()
    46. category_lines,all_categories,n_categories,all_letters,n_letters=data.processing()
    47. # if __name__=='__main__':
    48. # data=DataPreprocessing()
    49. # '''
    50. # 返回值一是一个字典,各个类型及其对应的所有名字
    51. # 返回值2是一个列表,所有类型的名字
    52. # 返回值3是类型的数量
    53. # '''
    54. # category_lines,all_categories,n_categories=data.processing()
    55. # print('种类数量:', n_categories, '所有的种类:', all_categories)
    56. # print("O'Néàl(unicode类型)转换到ASCII类型后为:", data.unicodeToAscii("O'Néàl"))

    三.构造神经网络

    这个神经网络比上一个RNN教程 中的网络增加了额外的类别张量参数,该参数与其他输入连接在一起。类别可以像字母一样组成 one-hot 向量构成张量输入。

    我们将输出作为下一个字母是什么的可能性。采样过程中,当前输出可能性最高的字母作为下一时刻输入字母。

    在组合隐藏状态和输出之后我们增加了第二个linear层o2o,使模型的性能更好。当然还有一个dropout层,参考这篇论文随机将输入部分替换为0 给出的参数(dropout=0.1)来模糊处理输入防止过拟合。 我们将它添加到网络的末端,故意添加一些混乱使采样特征增加。

    网络结构图:

    buildModel.py:

    1. import torch
    2. import torch.nn as nn
    3. #导入数据预处理之后的相关数据
    4. from dataPreprocessing import n_categories
    5. #*********************************** 参考这篇文章的图 https://www.cnblogs.com/lccxqk/p/14622532.html
    6. class RNN(nn.Module):
    7. # rnn = RNN(n_letters, 128, n_letters)说明有多少字符就有多少种输入情况,也就有多少种输出情况,所以最后需要一个Softmax层进行多元分类
    8. def __init__(self, input_size, hidden_size, output_size):
    9. super(RNN, self).__init__()
    10. self.hidden_size = hidden_size
    11. #其实是两层?只不过i2h和i2o其实可以看做一层,只不过传递的方向不一样
    12. self.i2h = nn.Linear(n_categories + input_size + hidden_size, hidden_size)
    13. self.i2o = nn.Linear(n_categories + input_size + hidden_size, output_size)
    14. self.o2o = nn.Linear(hidden_size + output_size, output_size)
    15. #防止过拟合
    16. self.dropout = nn.Dropout(0.1)
    17. #多元分类,# 对列做Softmax,最后得到的每行和为1;dim=0则每列和为1
    18. self.softmax = nn.LogSoftmax(dim=1)
    19. # 前向传播,三个参数都是行向量,且前俩是one-hot矩阵
    20. # 前向传播,三个参数都是行向量,结合这篇文章的前向传播那里的图进行分析 https://hanhan.blog.csdn.net/article/details/128062706
    21. # hidden就是图中的a,即向右传的激活值,
    22. # 一个单词的从左往右的所有字母依次进行前向传播,每次前向传播就对应图中的一列
    23. # 三个线性层其实是两层
    24. def forward(self, category, input, hidden):
    25. '''
    26. 运行以下代码查看torch.cat的功能,即把这三个行向量连接起来
    27. category=torch.zeros(1, 3)
    28. print(category)
    29. input=torch.ones(1,2)
    30. print(input)
    31. hidden=torch.zeros(1,2)
    32. print(hidden)
    33. input_combined = torch.cat((category, input, hidden), 1)
    34. print(input_combined)
    35. '''
    36. input_combined = torch.cat((category, input, hidden), 1)
    37. #往右传
    38. hidden = self.i2h(input_combined)
    39. #往上传
    40. output = self.i2o(input_combined)
    41. output_combined = torch.cat((hidden, output), 1)
    42. output = self.o2o(output_combined)
    43. output = self.dropout(output)
    44. output = self.softmax(output)
    45. return output, hidden
    46. def initHidden(self):
    47. #行向量(2维,即一行2列的矩阵)
    48. return torch.zeros(1, self.hidden_size)

    四.训练

    myTrain.py:

    1. import random
    2. from torch import nn
    3. from dataPreprocessing import category_lines,all_categories,n_categories,all_letters,n_letters
    4. import torch
    5. from buildModel import RNN
    6. #**********************************************************3.1 训练准备
    7. # 首先,构造一个可以随机获取成对训练数据(category, line)的函数。
    8. # 列表中的随机项
    9. def randomChoice(l):
    10. #某个类别里的随机的一个名字
    11. return l[random.randint(0, len(l) - 1)]
    12. # 从所有类别中获取随机类别和该类别对应的一个名
    13. def randomTrainingPair():
    14. #随机选一个类
    15. category = randomChoice(all_categories)
    16. # 上面选定的那个类里随机的一个名
    17. line = randomChoice(category_lines[category])
    18. return category, line
    19. '''
    20. 对于每个时间步长(即,对于要训练单词中的每个字母),网络的输入将是“(类别,当前字母,隐藏状态)”,输出将是“(下一个字母,
    21. 下一个隐藏状态)”。因此,对于每个训练集,我们将需要类别、一组输入字母和一组输出/目标字母。
    22. 在每一个时间序列,我们使用当前字母预测下一个字母,所以训练用的字母对来自于一个单词。
    23. 例如 对于 "ABCD",我们将创建 (“A”,“B”),(“B”,“C”),(“C”,“D”),(“D”,“EOS”))。
    24. 类别张量是一个<1 x n_categories>尺寸的one-hot张量。训练时,我们在每一个时间序列都将其提供给神经网络。
    25. 这是一种选择策略,也可选择将其作为初始隐藏状态的一部分,或者其他什么结构。
    26. '''
    27. # 类别的One-hot张量
    28. def categoryTensor(category):
    29. #category是类别名,即一个字符串,list.index(元素值)返回对应元素的下标
    30. li = all_categories.index(category)
    31. #一行n_categories列的张量(可以看作二维矩阵)
    32. tensor = torch.zeros(1, n_categories)
    33. tensor[0][li] = 1
    34. #返回这个类别对应的one-hot矩阵
    35. return tensor
    36. # 用于输入的从头到尾字母(不包括EOS)的one-hot矩阵,即单词的one-hot矩阵,即生成输入张量
    37. def inputTensor(line):
    38. #line是一个单词
    39. '''
    40. 对于张量而言,行向量其实就是个二维矩阵,所以一个三个元素的行向量就是一行3列的的2维矩阵,如下:
    41. tensor = torch.zeros(2, 1, 3)
    42. 所以上面这句代码的1和3就代表一个三个元素的行向量就是一行3列的的2维矩阵
    43. 然后那个2意思就是有俩一个三个元素的行向量就是一行3列的的2维矩阵
    44. 综合起来看就像一个2行3列的矩阵一样,但其实是个三维的
    45. '''
    46. tensor = torch.zeros(len(line), 1, n_letters)
    47. #li是单词的每个组成字符对应的下标
    48. for li in range(len(line)):
    49. # 单词的每个组成字符
    50. letter = line[li]
    51. '''
    52. 虽然是个三维矩阵,但是我们可以当作两维来看,第li行(对应第li个字母),0就对应第li行的那个行向量
    53. all_letters.find(letter)就代表这一行的这个字符对应的那一列
    54. '''
    55. tensor[li][0][all_letters.find(letter)] = 1
    56. #返回这个单词对应的one-hot矩阵
    57. return tensor
    58. # 用于目标的第二个结束字母(EOS)的LongTensor,即生成输出张量
    59. '''
    60. 下面这个函数的意思就是比如本来的的单词是book(最后还有一个结束符没写上,因为单词长度是4),这是输入张量
    61. 然后经过下面的这个函数我们得到的目标张量为ook,新旧张量的字符一一对应
    62. b o o k
    63. o o k
    64. '''
    65. def targetTensor(line):
    66. #all_letters.find(line[li])是字符在所有字符中对应的下标
    67. letter_indexes = [all_letters.find(line[li]) for li in range(1, len(line))]
    68. letter_indexes.append(n_letters - 1) # EOS
    69. return torch.LongTensor(letter_indexes)
    70. '''
    71. 为了方便训练,我们将创建一个randomTrainingExample函数,该函数随机获取(类别,行)的对并将它们转换为所需要的(类别,输入, 目标)格式张量。
    72. '''
    73. # 从随机(类别,行)对中创建类别,输入和目标张量
    74. def randomTrainingExample():
    75. # 随机获取一个类别和该类别的一个名字
    76. category, line = randomTrainingPair()
    77. # 类别的one-hot矩阵
    78. category_tensor = categoryTensor(category)
    79. # 输入单词的one-hot矩阵
    80. input_line_tensor = inputTensor(line)
    81. # 目标的one-hot矩阵
    82. target_line_tensor = targetTensor(line)
    83. return category_tensor, input_line_tensor, target_line_tensor
    84. #****************************************************************3.2 训练神经网络
    85. '''
    86. 和只使用最后一个时刻输出的分类任务相比,这次我们每一个时间序列都会进行一次预测,所以每一个时间序列我们都会计算损失。
    87. autograd 的神奇之处在于您可以在每一步中简单地累加这些损失,并在最后反向传播。
    88. '''
    89. #损失函数
    90. criterion = nn.NLLLoss()
    91. #学习率
    92. learning_rate = 0.0005
    93. #我们自己写的RNN模型的实例,n_letters是所有字符个数
    94. rnn = RNN(n_letters, 128, n_letters)
    95. def train(category_tensor, input_line_tensor, target_line_tensor):
    96. '''
    97. from dataPreprocessing import all_letters,n_letters
    98. def targetTensor(line):
    99. #all_letters.find(line[li])是字符在所有字符中对应的下标
    100. letter_indexes = [all_letters.find(line[li]) for li in range(1, len(line))]
    101. letter_indexes.append(n_letters - 1) # EOS
    102. return torch.LongTensor(letter_indexes)
    103. print(targetTensor("apple"))
    104. print(targetTensor("apple").unsqueeze_(-1))
    105. '''
    106. #把上面的代码单独在一个文件执行一下理解.unsqueeze_(-1)在干嘛
    107. target_line_tensor.unsqueeze_(-1)
    108. hidden = rnn.initHidden()
    109. # 梯度清零
    110. rnn.zero_grad()
    111. #损失
    112. loss = 0
    113. #循环次数就是单词所含的字母个数
    114. '''
    115. tensor = torch.zeros(2, 1, 3)
    116. print(tensor.size(0)) 输出2
    117. 即单词的one-hot矩阵每一行对应一个字母的one-hot
    118. '''
    119. for i in range(input_line_tensor.size(0)):
    120. #前向传播,三个参数都是行向量,结合这篇文章的前向传播那里的图进行分析 https://hanhan.blog.csdn.net/article/details/128062706
    121. #hidden就是图中的a,所以本次循环用的是上一次循环的hidden,即向右传激活值的过程;input_line_tensor[i]对应图中的x^
    122. #一个单词的从左往右的所有字母依次进行前向传播,每次前向传播就是图中的一列
    123. #三个线性层其实是两层
    124. output, hidden = rnn(category_tensor, input_line_tensor[i], hidden)
    125. l = criterion(output, target_line_tensor[i])
    126. loss += l
    127. #反向传播,计算偏导
    128. loss.backward()
    129. #梯度下降
    130. #权重=权重-学习率*成本函数对此权重的偏导
    131. #训练过程和以前一样,要说的是这里没有用pytorch自带的优化器,而是用下面循环来参数更新,但是运行时会出现报警(但程序还是可以运行)
    132. for p in rnn.parameters():
    133. p.data.add_(-learning_rate, p.grad.data)
    134. return output, loss.item() / input_line_tensor.size(0)
    135. # 为了跟踪训练耗费的时间,我添加一个timeSince(timestamp)函数,它返回一个人类可读的字符串:
    136. import time
    137. import math
    138. def timeSince(since):
    139. now = time.time()
    140. s = now - since
    141. m = math.floor(s / 60)
    142. s -= m * 60
    143. return '%dm %ds' % (m, s)
    144. #*************************************************** 待训练完成,模型保存之后,将下列代码注释掉
    145. '''
    146. 训练过程和平时一样。多次运行训练,等待几分钟,每print_every次打印当前时间和损失。
    147. 在all_losses中保留每plot_every次的平均损失,以便稍后进行绘图。
    148. '''
    149. #迭代十万次
    150. n_iters = 100000
    151. print_every = 5000
    152. plot_every = 500
    153. all_losses = []
    154. total_loss = 0 # Reset every plot_every iters
    155. start = time.time()
    156. for iter in range(1, n_iters + 1):
    157. #星号的作用是将元组变为一个一个的值
    158. '''
    159. def fun():
    160. return 'a',1,"apple";
    161. print(fun()) #('a', 1, 'apple')
    162. print(*fun()) #a 1 apple
    163. '''
    164. output, loss = train(*randomTrainingExample())
    165. total_loss += loss
    166. if iter % print_every == 0:
    167. print('%s (%d %d%%) %.4f' % (timeSince(start), iter, iter / n_iters * 100, loss))
    168. if iter % plot_every == 0:
    169. #最近plot_every次的平均损失(加入到记录损失的列表)
    170. all_losses.append(total_loss / plot_every)
    171. total_loss = 0
    172. #******************************************************* 3.3 损失数据作图
    173. # 从all_losses得到历史损失记录,反映了神经网络的学习情况:
    174. import matplotlib.pyplot as plt
    175. plt.figure()
    176. plt.plot(all_losses)
    177. plt.show()
    178. #******************************************************* 3.4 保存模型
    179. torch.save(rnn.state_dict(), './model/myRNN.pth')

    可以看到训练完成之后,相应目录下已经保存了模型的参数文件:

    五.网络采样(预测)

    我们每次给网络提供一个字母并预测下一个字母是什么,将预测到的字母继续输入,直到得到EOS字符结束循环。

    • 用输入类别、起始字母和空隐藏状态创建输入张量。

    • 用起始字母构建一个字符串变量 output_name

    • 得到最大输出长度,
        * 将当前字母传入神经网络
        * 从前一层得到下一个字母和下一个隐藏状态
        * 如果字母是EOS,在这里停止
        * 如果是一个普通的字母,添加到output_name变量并继续循环

    • 返回最终得到的名字单词

    另一种策略是,不必给网络一个起始字母,而是在训练中提供一个“字符串开始”的标记,并让网络自己选择起始的字母。

    predict.py:

    1. import torch
    2. from myTrain import categoryTensor,inputTensor
    3. from dataPreprocessing import n_letters,all_letters
    4. from buildModel import RNN
    5. #*********************************************************** 4.网络采样(即预测)
    6. #我们自己写的RNN模型的实例,n_letters是所有字符个数
    7. rnn = RNN(n_letters, 128, n_letters)
    8. #加载已经训练好的模型参数
    9. rnn.load_state_dict(torch.load('./model/myRNN.pth'))
    10. #eval函数(一定用!!!)的作用请参考 https://blog.csdn.net/lgzlgz3102/article/details/115987271
    11. rnn.eval()
    12. max_length = 20
    13. # 来自类别和首字母的样本
    14. def sample(category, start_letter='A'):
    15. #表明当前计算不需要反向传播,使用with torch.no_grad()之后,强制后边的内容不进行计算图的构建
    16. #一般计算网络结果(预测)时,不需要反向传播,所以就就用with torch.no_grad()
    17. with torch.no_grad(): # no need to track history in sampling
    18. category_tensor = categoryTensor(category)
    19. input = inputTensor(start_letter)
    20. hidden = rnn.initHidden()
    21. output_name = start_letter
    22. for i in range(max_length):
    23. output, hidden = rnn(category_tensor, input[0], hidden)
    24. topv, topi = output.topk(1)
    25. if i==0:
    26. print('topv:',topv)
    27. print('topi',topi)
    28. topi = topi[0][0]
    29. #即topi是的下标时,就可以结束了
    30. if topi == n_letters - 1:
    31. break
    32. else:
    33. letter = all_letters[topi]
    34. output_name += letter
    35. #上一个单元预测出的字符作为下一个单元的输入
    36. input = inputTensor(letter)
    37. return output_name
    38. # 从一个类别和多个起始字母中获取多个样本
    39. def samples(category, start_letters='ABC'):
    40. for start_letter in start_letters:
    41. print(sample(category, start_letter))
    42. samples('Russian', 'RUS')
    43. samples('German', 'GER')
    44. samples('Spanish', 'SPA')
    45. samples('Chinese', 'CHI')

    输出:

    1. Rovakov
    2. Uakovakov
    3. Shakovak
    4. Garter
    5. Erenger
    6. Romer
    7. Santer
    8. Parez
    9. Allan
    10. Chang
    11. Han
    12. Iua

  • 相关阅读:
    【STM32】定时器与PWM的LED控制
    测试报告。
    辅助驾驶功能开发-功能规范篇(16)-2-领航辅助系统NAP-安全接管策略
    redis持久化储存(RDB、AOF)和主从复制
    长三角实现区块链电子医疗票据互联互通,蚂蚁链提供技术支持
    Richardson Software RazorSQL 10.0 Crack
    springboot 发布tomcat(war包)
    Linux 内核irq_stack遍历
    数据结构题目收录(二十)
    如何设置微信自动回复?教你快速上手!
  • 原文地址:https://blog.csdn.net/weixin_44593822/article/details/128189196