Loading web-font TeX/Main/Italic

基于LSTM的中文姓名性别预测

基于LSTM的中文姓名性别预测

写在前面

这是本人编写的首个基于调库的机器学习程序,算是一个简单的练手项目。

本程序可以根据输入的中文姓名推测其对应的性别

根据人的名字判断人的性别是一个很有意思的工作,我们可以用朴素贝叶斯法,SVM法或神经网络解决。Python有个第三方库叫做ngender,它采用朴素贝叶斯进行预测,然而,该模型的准确度仍有待提高。

本人基于Pytorch,采用embedding+LSTM+Linear的神经网络模型进行性别的预测,准确率达到了88%

代码已经上传至GitHub,[这是GitHub链接](AlphaINF/name2gender: Gender prediction of chinese name based on LSTM (github.com))

本人将在下文中,详细介绍模型的结构和使用方法,以及我对每一个模块的理解

运行方法

将仓库中全部文件下载到本地

直接运行main.py,输入姓名即可进行预测

训练方法

将仓库中全部文件下载到本地

调整finetune中的data=xxx_loader及其对应的文件名,选择对应的文件作为训练集

调整test中的data=xxx_loader及其对应的文件名,选择对应的文件作为测试集

调整finetune中的模型保存规则(可以查看torch.save部分)

运行finetune.py

文件结构

本程序包含以下几个文件,文件结构如下图所示

1669277697252

每个文件的用途如下表所示

文件名 用途
name2gender.py 用于保存模型的结构
main.py 直接运行即可输入名字进行性别预测
test.py 用于运行测试集的代码
finetune.py 用于训练的代码
utils.py 一些工具(比如csv的读取工具)
net.pth 训练好的神经网络
ccnc.csv 数据集1(采用ccnc_loader进行读取),包含有约350w组数据,每组数据的格式为(姓氏,名字,姓名,性别),采用换行和tab间隔
train.csv 数据集2(采用csv_loader进行读取),包含有约20w组数据,每组数据的格式为(姓名,性别),采用换行和逗号间隔
test.csv/ccnc-tiny.csv 测试集

数据输入和预处理

笔者找到了两个数据集,一个是ccnc数据集,另一个是train.csv/test.csv 。经实测train/test效果更加

ccnc.csv

ccnc.csv,包含有365.8w条数据,ccnc-tiny是ccnc中截取的数据。其格式如下图所示

姓 名 全名 性别(第一行可以忽略)
陈 品如 陈品如 M
陈 祥旭 陈祥旭 M
陈 晓 陈晓 M
陈 东慧 陈东慧 M
陈 镇彬 陈镇彬 M

分隔符为tab

在utils.py中,笔者为其编写了数据读取代码

def ccnc_loader(file_name):
logging.info('[DataLoader]: loading data, path =' + file_name)
fp = open(file_name, "r", encoding='utf-8', errors='ignore')
next(fp)
all = fp.read().split('\n')
fp.close()
output = []
for line in all:
line_element = str(line).split('\t')
# print(line_element)
try:
label = ['F', 'M'].index(line_element[3])
output.append((line_element[2], line_element[3]))
except:
label = 0
logging.info('[DataLoader]: finished loading, len = ', len(output))
return output

注意:ccnc.csv中的性别标签包含有U(unknown),所以要进行特判

笔者将label[2]和label[3] (也就是姓名和性别)存入dataset中,大家也可以将其改成存入名字和性别。

train.csv

train.csv中包含20w条数据,test.csv中包含2w条数据,其格式如下

赵伏琴,女
钱沐杨,男
孙竹珍,女
李潮阳,男
蔡凤灿,男
范素菊,女
赵朕林,女
陆好骋,女
王舒梅,女
孙国江,男

分隔符为逗号

在utils中,笔者为其编写了数据读取代码

def csv_loader(file_name):
fp = open(file_name, "r", encoding='utf-8', errors='ignore')
all = fp.read().split('\n')
fp.close()
output = []
for line in all:
line_element = str(line).split(',')
# print(line_element)
try:
label = ['女', '男'].index(line_element[1])
output.append((line_element[0], ['F', 'M'][label]))
except:
label = 0
logging.info('[DataLoader]: finished loading, len = ', len(output))
return output

注意:性别的存储进行了转换,转换为了ccnc的格式

Dataset和DataLoader

DataLoader是Pytorch自带的数据读取器,需要传入dataset(一个数据集)参数和batch_size参数,DataLoader会对数据进行自动切分为多份,每一份中包含batch_size组数据。

dataset必须是类Dataset的派生类,用于装填自己及需要的数据。它的作用是:只要告诉它数据在哪里(初始化),就可以像使用iterator一样去拿到数据,继承该类后,需要重载__len__()以及__getitem__

笔者在学习的过程中,曾认为自己写一个加载数据的工具似乎也没有多“困难”,为何大费周章要继承pytorch中类,按照它的规则加载数据呢?总结一下就是:

当数据量很大的时候,单进程加载数据很慢

一次全加载过来,会占用很大的内存空间(因此dataloader是一个生成器,惰性加载)

在进行训练前,往往需要一些数据预处理或数据增强等操作,pytorch的dataloader已经封装好了,避免了重复造轮子

笔者的Dataset类定义代码如下:

class NameDataset(Dataset):
def __init__(self, data):
self.data = data
def __getitem__(self, item):
data = self.data[item]
name = data[0]
label = ['F', 'M'].index(data[1])
return name, label
def __len__(self):
return len(self.data)

笔者调用数据读取,创建Dataset和DataLoader的代码如下:

data = csv_loader('dataset/train.csv')
data_set = NameDataset(data)
batch_size = 64
data_loader = DataLoader(data_set, batch_size=batch_size, shuffle=True)

数据格式转换

存入DataLoader中的数据,DataLoader会对数据进行一些预处理。

比如说某一批数据中,性别的标签为[0,1,0,0,0,1,1],经过DataLoader后,数据会变成tensor([0, 1, 0, 0, 0, 1, 1]),tensor类型的数据才能送入神经网络中进行训练。

但对于存储姓名的字符串,DataLoader并不会进行处理,需要我们手动转换

下方的函数,输入是姓名表names和性别表labels,输出是tensor格式的names表,labels表和长度表。

def name_to_list(name):
character_list = []
for ch in name:
try:
val = ch.encode(encoding='ansi')
character_list.append((val[0] - 128) << 8 | val[1])
except:
character_list.append(0)
return character_list
def value_to_tensor(names, labels):
name_list = []
for name in names:
name_list.append(name_to_list(name))
lengths = np.array([len(name) for name in name_list])
max_len = max(lengths)
name_tensors = torch.zeros(len(names), max_len)
for i, idx in enumerate(name_list):
for j, e in enumerate(idx):
name_tensors[i][j] = e
name_tensors = name_tensors.to(torch.long)
lengths = torch.from_numpy(lengths).to(torch.long)
return name_tensors, labels, lengths

笔者采用的姓名转码方式为:对于姓名'刘子晶',先将其视为字符数组['刘','子','晶'],然后对于每个字,获得在ANSI编码下的数值[0xC1F5,0xD7D3,0xBEA7],然后对每个数值减去0x8000,即完成转码,输出对应的数值[0x41F5,0x57D3,0x3EA7]

对于输出的数值,在value_to_tensor函数中,还要将每一组姓名的长度对齐,并转化为tensor格式,否则输入神经网络的时候会报错。

模型结构

我们可以通过mane2gender.py来了解模型的结构

import torch.nn as nn
class name2gender(nn.Module):
def __init__(self, input_size, embedding_size, rnn_hidden_size, hidden_size, output_size=2):
super(name2gender, self).__init__()
self.embeddings = nn.Embedding(input_size, embedding_size, padding_idx=0)
self.drop = nn.Dropout(p=0.1)
self.rnn = nn.LSTM(input_size=embedding_size, hidden_size=rnn_hidden_size, batch_first=True)
self.linear1 = nn.Linear(rnn_hidden_size, hidden_size)
self.activation = nn.ReLU()
self.linear2 = nn.Linear(hidden_size, output_size)
self.output_size = output_size
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, name, length):
now = self.embeddings(name)
now = self.drop(now)
input_packed = nn.utils.rnn.pack_padded_sequence(now, length, batch_first=True, enforce_sorted=False)
_, (ht, _) = self.rnn(input_packed, None)
out = self.linear1(ht)
out = self.activation(out)
out = self.linear2(out)
out = out.view(-1, self.output_size)
out = self.softmax(out)
return out

笔者构建的模型中,input_size等参数如下:

model = name2gender(32768, 256, 128, 50)

笔者采用的模型可以分为几个部分,首先是embedding层(将字转化为字向量),dropout层(随机将一些位置的数值设为0),rnn层(进行LSTM),全连接层1,激活函数,全连接层2,softmax层,最后的输出有两个节点,分别表示该名字为男或者为女的概率。

下面将对模型中每个部分进行简单的介绍

数据输入

假设输入的名字为"刘子晶",则会按照上述的过程(数据格式转换过程),将这三个字变为一个1*3的向量,并将该向量送入embedding层

embedding层

embedding层是一个将单字转化为字向量(可以理解为字具有的属性)的过程,这个过程被称为词嵌入

举个例子:琪(ANSI编码下为0xE7F7)字,具有的属性包括:部首(王),笔画数(12),右半边长相(其),用于女性的比例(我不知道)……

处理出了单字的字向量后,我们可以通过求两个向量之间的点积以获取两个字之间的相似度(比如说琪和祺之间就有较高的相似度)

从单字到字的属性的过程,就是embedding层的用途

embedding层有一个很神奇的地方:字的属性是可以通过学习得到的,不需要我们手动设置

笔者采用的embedding层大小为32768*256,表示输入的汉字最多有32768个,对于输入的每一个字,将会输出一个包含256个float的向量,用于表示这个字的属性。

至于汉字如何转化为[0,32767]间的整数,可以看下方的“数据输入”,由于字库较小,输入非常非常生僻的字可能导致信息丢失。

关于更多embedding层的知识可以见[这篇blog]((34条消息) [文本分类]深入理解embedding层的模型、结构与文本表示_征途黯然.的博客-CSDN博客_embedding模型)

还是以一个三字的输入举例:输入的是一个1×3的向量,embedding层会先将这些输入,转为1×3×32768的01向量(对于任意的(i,j,k),当i,j确定时,k只有一个位置为1,即与编码对应的字符为1),随后进入embedding层进行矩阵乘法,最后输出是1×3×256的向量,也就是每个字的编码变成了这个字对应的字向量。

如果是m组三字输入数据,则输入是m×3的向量,输出是m×3×256的向量

注意这个256是由上文中的embedding_size确定的

dropout层

dropout的功能是:对于输入的向量,每个元素有p的概率会变为0,本代码中p设置为0.1

dropout层的功能在于:在训练时,随机让一定数量的值归零,可以提高网络的泛化能力,可以避免过拟合

注:在测试的时候,一般不会启用dropout层,笔者在这里并没有进行判断

输入dropout层的数据为m×3×256(m含义如上文所示),输出的规模并没有出现变化

LSTM层

LSTM是一种升级的RNN神经网络,。相比一般的神经网络(比如全连接的BP)来说,他能够处理序列变化的数据。比如某个单词的意思会因为上文提到的内容不同而有不同的含义,RNN就能够很好地解决这类问题。

如果你学过最简单的bp神经网络,你可以通过这两个视频来学习RNN和LSTM,动画做的贼好!

视频一:RNN的学习视频【循环神经网络】5分钟搞懂RNN,3D动画深入浅出_哔哩哔哩_bilibili

视频二:LSTM的学习视频【LSTM长短期记忆网络】3D模型一目了然,带你领略算法背后的逻辑_哔哩哔哩_bilibili

这里有一份c++的RNN源代码,可以用来看原理链接

对于输入数据,假设输入是m×3×256的数据(即为m组数据,每组数据有三个字,每个字由一个长度为256的字向量构成),则输出将变为m×128,其中128由rnn_hidden_size确定,3的消失是因为:每一组数据,是按照“字”的先后顺序输入进RNN中的。其中m是数据的组数

全连接层1

从LSTM连出,我们决定连一个全连接层

通过该全连接层,数据的规模将会从m×128下降为m×50,并且融合更多的信息

至于这东西为啥有用?(问就是玄学)

激活函数

激活函数可以将全连接层中的线性变换,转化为非线性变换

可以看这篇文章形象的解释神经网络激活函数的作用是什么? - 知乎 (zhihu.com)

至于为何LSTM到全连接层1不需要线性变换?这是因为LSTM内自带了激活函数(

全连接层2

从激活后的全连接层1中,对于一组数据,我们仍有50个函数(m组就是m×50)

但是我们只需要两个输出(男性概率,女性概率)

所以我们需要再进行一次全连接,将m×50变为m×2

softmax层

对于剩下的两个数,求解一次softmax(可以去查查这个是做什么的)

softmax分类器可以扩大分数的差距,使得分类效果更加明显

经过softmax后,就是最终输出的答案了(男性概率,女性概率)

模型运行

我们首先输入个名字

然后把这个名字转成tensor格式,输入进这个模型中

模型将按照咱们的定义,依次把数据流过模型的每一层

最后的输出即为男性/女性的概率

模型训练

模型的训练过程可以这样理解

1,首先运行模型

2,根据模型的预测值和实际值进行比较,求出差值loss(一般采用交叉熵求解)

3,根据差值,乘上一个训练系数,进行反向传播(可以理解为:如果求出的值大了,就对网络进行一些调节,这样下次的输出就不会那么大了)

代码如下(详见finetune.py)

out = model(name_tensors, name_lengths).view(-1,2)
loss = loss_func(out, labels)
total_loss += loss
optimizer.zero_grad()
loss.backward()
optimizer.step()

训练效果

1669277697252

posted @   AlphaInf  阅读(956)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示