比赛概述

本比赛为个人练习赛,主要针对于数据新人、尤其文本处理方面的新手进行自我练习、自我提高,与大家切磋。

任务类型:自然语言处理、二元分类

背景介绍:
我们每天会接触不同的人,有热情的面孔,也有冰冷的名片。作为一个说着中国话的中国人,我们有着常年累月的积累和对文字的理解,通过名字判断(猜测)性别,并非难事。可是,对于一个冰冷的机器,它能够根据名字判断性别吗?这个练习赛就是根据中文名字(姓已经省略),判断性别

数据来源:
数据来自网络爬虫。出于隐私保护等目的,姓已经被省略。训练数据中含有很少量的杂质(也就是错误的数据点)。标题图片来源:fanfou。

数据下载

数据文件(三个):

  • train.txt 训练集,文件大小 1.7mb
  • test.txt 预测集, 文件大小 0.4mb
  • sample_submit.csv 提交示例 文件大小 0.2mb

训练集中共有120000条样本,预测集中有32040条样本。

变量说明:

变量名解释
id编号
name名字(姓已经被隐去)
gender表示该姓名对应的性别。0表示女生,1表示男生。在test.txt中,这是需要被预测的标签。请注意:名字与性别均来自于网络爬虫,数据可能有杂质或者重复。

评价方法

您提交的结果为每个姓名的预测性别,0代表女性,1代表男性。评价方法为准确率(accuracy)。

准确率的取值范围是0到1。越接近1,说明模型预测的结果越接近真实结果。

$$准确度 = \frac{预测正确数}{总预测数}$$

准确度=预测正确数总预测数准确度=预测正确数总预测数

比如说,真实情况是(小芳:0), (钢:1),(俏梅:0),(德华:1)。
你预测结果是(小芳:0), (钢:0),(俏梅:0),(德华:1),那么准确度是3/4=0.75。

提交结果

提交前请确保预测结果的格式与sample_submit.csv中的格式一致,以及提交文件后缀名为csv。文件大小不超过5mb。

您提交的结果为每个姓名的预测性别,0代表女性,1代表男性。


解决方案

一、 利用朴素贝叶斯对名字进行性别预测


1. 条件概率与贝叶斯定理

对于事件 A 和 B,当 B 发生的情况下, A 发生的条件概率为

$$P(A|B) =\frac{P(AB)}{P(B)}$$

如果把 P(AB) 表示为 P(B|A)P(A),那么

$$P(A|B)=\frac{P(B|A)P(A)}{P(B)}$$

2. 朴素贝叶斯

朴素贝叶斯是一个基于贝叶斯定理的分类算法,其基本假设是所有特征是相互独立的。 举个例子来说,有一个二元分类问题,每个样本只有两个二元特征 $X_1$ 和$X_2$。若已知一个样本 $(X_1=1,X_2=0)$, 我们要预测它的标签为 1 的概率,就是等价于去计算

$$P(Y=1|X1=1,X2=0)$$

根据贝叶斯定理,我们可得

$$P(Y=1|X_1=1,X_2=0)=\frac{P(Y=1)P(X_1=1,X_2=0|Y=1)}{P(X_1=1,X_2=0)}$$

其中$P(Y=1)$被称为先验(prior),$P(X_1=1,X_2=0|Y=1)$ 被称为似然(likelyhood),$P(X_1=1,X_2=0)$被成为证据(evidence)。

因为我们假设所有特征独立,所以我们可以把$P(Y=1|X_1=1,X_2=0)$写成

$$P(Y=1|X_1=1,X_2=0)=\frac{P(Y=1)P(X_1=1|Y=1)P(X_2=0|Y=1)}{P(X_1=1)P(X_2=0)}$$

推广到更普遍的情况下,假设数据有k个特征,

$$P(Y|X_1,X_2,⋯,X_n)=\frac{1}{Z}P(Y)\prod_{i=1}^{n}P(X_i|Y)$$

其中 $Z$ 是缩放因子,使得概率和为1。

对于一个分类问题,如果我们只需要得到其标签,我们只需要求解

$$y_{pred} = argmax_{y}P(Y=y)\prod_{i=1}^{n}P(X_i|Y=y)$$

3. 实战练习

下面我们利用朴素贝叶斯对“机器读中文:根据名字判断性别”中的数据进行预测。首先下载,并读取数据。

# -*- coding: utf-8 -*-

import pandas as pd
from collections import defaultdict
import math

# 读取train.txt
train = pd.read_csv('train.txt')
test = pd.read_csv('test.txt')
submit = pd.read_csv('sample_submit.csv')

看看训练集中的数据长什么样

train.head(10)
idnamegender
01闳家1
12玉璎0
23于邺1
34越英0
45蕴萱0
56子颀0
67靖曦0
78鲁莱1
89永远1
910红孙1
# 把数据分为男女两部分
names_female = train[train['gender'] == 0]
names_male = train[train['gender'] == 1]

# totals用来存放训练集中女生、男生的总数
totals = {'f': len(names_female),
          'm': len(names_male)}

分别计算在所有女生(男生)的名字当中,某个字出现的频率。这一步相当于是计算 $P(X_i|女生)P(X_i|女生)$ 和 $P(X_i|男生)P(X_i|男生)$

frequency_list_f = defaultdict(int)
for name in names_female['name']:
    for char in name:
        frequency_list_f[char] += 1. / totals['f']

frequency_list_m = defaultdict(int)
for name in names_male['name']:
    for char in name:
        frequency_list_m[char] += 1. / totals['m']
        
print(frequency_list_f['娟'])
#0.004144009000562539
print(frequency_list_m['钢'])
#0.0006299685015749209

上面两个例子说明 $P(名字中含有娟|女生)=0.004144$ ,$P(名字中含有钢|男生)=0.0006299$

考虑到预测集中可能会有汉字并没有出现在训练集中,所以我们需要对频率进行Laplace平滑(什么是Laplace平滑)。

def LaplaceSmooth(char, frequency_list, total, alpha=1.0):
    count = frequency_list[char] * total
    distinct_chars = len(frequency_list)
    freq_smooth = (count + alpha ) / (total + distinct_chars * alpha)
    return freq_smooth

回顾第2节中的式子

$$P(Y)\prod_{i=1}^{n}P(X_{i}|Y)$$,

在性别预测中,每个样本中大量的特征都是 0。比如说只有$X_2=1$,其他都为 0,那么

$$y_{pred} = argmax_{y}P(Y=y)P(X_{2}=1|Y=y)\frac{\prod_{i=1}^{n}P(X_{i}=0|Y=y)}{P(X_{2}=0|Y=y)}$$

由于$P(X_i)$ 的数值通常较小,我们对整体取对数(防止浮点误差),可得

$$log(P(Y=y)) + \sum_{i=1}^{n}(logP(X_{i}=0|Y=y) + logP(X_{2}=1 | Y=y) - logP(X_{2}=0|Y=y))$$

如果一个人的名字中有两个字,假设 $X_5=1,X_10=1$,其余为0,那么该名字的对数概率表达式为

$$log(P(Y=y)) + \sum_{i=1}^{n}logP(X_{i}=0|Y=y)$$

$$+ logP(X_{5}=1|Y=y) - logP(X_{5}=0|Y=y) + logP(X_{10}=1|Y=y)-logP(X_{10}=0|Y=y)$$

对于一种性别,$logP(Y=y) + \sum_{i=1}^{n}logP(X_{i}=0|Y=y)$只需要计算一次。为了方面,我们将其数值存放在 $bases$ 当中

base_f = math.log(1 - train['gender'].mean())
base_f += sum([math.log(1 - frequency_list_f[char]) for char in frequency_list_f])

base_m = math.log(train['gender'].mean())
base_m += sum([math.log(1 - frequency_list_m[char]) for char in frequency_list_m])

bases = {'f': base_f, 'm': base_m}

对于 $log⁡P(X_i=1|Y)−log⁡P(X_i=0|Y)$ 部分,我们利用如下函数计算

def GetLogProb(char, frequency_list, total):
    freq_smooth = LaplaceSmooth(char, frequency_list, total)
    return math.log(freq_smooth) - math.log(1 - freq_smooth)

最后我们只需要组合以上函数,实现

$$y_{pred} = argmax_{y}P(Y=y)P(X_{2}=1|Y=y)\frac{\prod_{i=1}^{n}P(X_{i}=0|Y=y)}{P(X_{2}=0|Y=y)}$$

def ComputeLogProb(name, bases, totals, frequency_list_m, frequency_list_f):
    logprob_m = bases['m']
    logprob_f = bases['f']
    for char in name:
        logprob_m += GetLogProb(char, frequency_list_m, totals['m'])
        logprob_f += GetLogProb(char, frequency_list_f, totals['f'])
    return {'male': logprob_m, 'female': logprob_f}

def GetGender(LogProbs):
    return LogProbs['male'] > LogProbs['female']

result = []
for name in test['name']:
    LogProbs = ComputeLogProb(name, bases, totals, frequency_list_m, frequency_list_f)
    gender = GetGender(LogProbs)
    result.append(int(gender))

submit['gender'] = result

submit.to_csv('my_NB_prediction.csv', index=False)

最后结果输出在'my_NB_prediction.csv'中。不如上传到比赛页面看看结果哦。

我们可以看看预测结果如何。

test['pred'] = result
test.head(20)
idnamepred
00辰君0
11佳遥0
22淼剑1
33浩苳1
44俪妍0
55秉毅1
66妍艺0
77海防1
88壬尧1
99珞千0
1010义元1
1111才君1
1212吉喆1
1313少竣1
1414创海1
1515熙兰0
1616家冬1
1717方荧1
1818介二1
1919钰泷1

完整代码如下

# -*- coding: utf-8 -*-

import pandas as pd
from collections import defaultdict
import math

# 读取train.txt
train = pd.read_csv('train.txt')
test = pd.read_csv('test.txt')
submit = pd.read_csv('sample_submit.csv')

#把数据分为男女两部分
names_female = train[train['gender'] == 0]
names_male = train[train['gender'] == 1]

totals = {'f': len(names_female),
          'm': len(names_male)}

frequency_list_f = defaultdict(int)
for name in names_female['name']:
    for char in name:
        frequency_list_f[char] += 1. / totals['f']

frequency_list_m = defaultdict(int)
for name in names_male['name']:
    for char in name:
        frequency_list_m[char] += 1. / totals['m']

def LaplaceSmooth(char, frequency_list, total, alpha=1.0):
    count = frequency_list[char] * total
    distinct_chars = len(frequency_list)
    freq_smooth = (count + alpha ) / (total + distinct_chars * alpha)
    return freq_smooth

def GetLogProb(char, frequency_list, total):
    freq_smooth = LaplaceSmooth(char, frequency_list, total)
    return math.log(freq_smooth) - math.log(1 - freq_smooth)

def ComputeLogProb(name, bases, totals, frequency_list_m, frequency_list_f):
    logprob_m = bases['m']
    logprob_f = bases['f']
    for char in name:
        logprob_m += GetLogProb(char, frequency_list_m, totals['m'])
        logprob_f += GetLogProb(char, frequency_list_f, totals['f'])
    return {'male': logprob_m, 'female': logprob_f}

def GetGender(LogProbs):
    return LogProbs['male'] > LogProbs['female']


base_f = math.log(1 - train['gender'].mean())
base_f += sum([math.log(1 - frequency_list_f[char]) for char in frequency_list_f])

base_m = math.log(train['gender'].mean())
base_m += sum([math.log(1 - frequency_list_m[char]) for char in frequency_list_m])

bases = {'f': base_f, 'm': base_m}


result = []
for name in test['name']:
    LogProbs = ComputeLogProb(name, bases, totals, frequency_list_m, frequency_list_f)
    gender = GetGender(LogProbs)
    result.append(int(gender))

submit['gender'] = result

submit.to_csv('my_NB_prediction12.csv', index=False)
Last modification:July 27, 2021
如果觉得我的文章对你有用,请随意赞赏