iMind Developers Blog

iMind開発者ブログ

Pythonで簡易な名前からの性別判定器を書く

概要

名前から性別を判定するのって簡単にできるのかな、ということで簡易な判定器を書いて試してみる。

バージョン情報

  • Python 3.6.8
  • scikit-learn==0.19.1
  • xgboost==0.72.1

テストデータ

名前一覧とか名前ランキング的なページをスクレイピングして27,326件の日本人のファーストネームを収集。

下記のような形式で保存する。

name yomi    label
太郎  たろう   0
愛 あい  1

label=0が男性, label=1が女性。

データについてはスクレイピングしたものなので公開は控える。

bi-gramのみを利用した予測

まずは単純に下記のように名前の読みをbi-gramにして特徴としてみる。

"たかし" → ["たか", "かし"]
"ようこ" → ["よう", "うこ"]

分析器は下記の3つを利用。

  • logistic regression
  • SVM
  • XGBoost

名前をbi-gramにするコード。

import pandas as pd

# ファイル読み込み
df = pd.read_csv('data/name_gender_estimator/name_gender.tsv', sep='\t')

# 読みとlabelだけにしてランダムソート
df = df[['yomi', 'label']].drop_duplicates()
df = df.sample(frac=1).reset_index(drop=True)

# bi-gramを取る
def bigram(text):
    return [text[i:i+2] for i in range(len(text) - 1)]
df['bi_yomi'] = df.yomi.apply(bigram)
df.head(3)

実行結果

index yomi label bi_yomi
0 せんま 0 [せん, んま]
1 いさや 0 [いさ, さや]
2 なの 1 [なの]

MultiLabelBinarizer(文字列を特徴IDとして扱うのを補助してくれる)を使ってfitしておく。

from sklearn.preprocessing import MultiLabelBinarizer
mlb = MultiLabelBinarizer()
mlb.fit(df.bi_yomi)
mlb.classes_

実行結果

array(['あい', 'あお', 'あか', ..., 'んや', 'んゆ', 'んり'], dtype=object)

5-foldで学習して評価する。probabilityが0.5付近の結果は判定不能とする。

from sklearn.model_selection import KFold

# 渡されたscikit-learnのclassifierに対して学習して評価する
# probabilityが0.6以上だったら判定できたことにする
def train_and_test(classifier, df, mlb, threshold=0.6):
    kf = KFold(n_splits=5)
    for train, test in kf.split(df):
        # 訓練データとテストデータにsplit
        train_df = df.loc[train]
        test_df = df.loc[test]

        # MultiLabelBinarizerを使ってベクトルに
        train_features = mlb.transform(train_df.bi_yomi)
        test_features = mlb.transform(test_df.bi_yomi)

        # 学習
        classifier.fit( train_features, train_df.label )

        # 評価
        test_proba = classifier.predict_proba(test_features)
        test_df['proba_male'] = [p[0] for p in test_proba]
        test_df['proba_female'] = [p[1] for p in test_proba]
        test_df['predict'] = -1
        # probabilityがthreshold以上の場合だけ判定結果を採用(ここでは0.6)
        test_df.loc[test_df.proba_male >= threshold, 'predict'] = 0
        test_df.loc[test_df.proba_female >= threshold, 'predict'] = 1
        all_len = len(test_df)
        predictable = len(test_df[test_df.predict != -1])
        tp = sum(test_df['predict'] == test_df['label'])
        fp = sum((test_df.predict != -1) & (test_df['predict'] != test_df['label']))
        class_name = str(classifier.__class__).split('.')[-1][:-2]
        print('{}: all={}, predictable={}, precision={:.03f}, recall={:.03f}'.format(
                class_name, all_len, predictable, tp / predictable, tp / all_len))

# logistic regression
from sklearn.linear_model import LogisticRegression
classifier = LogisticRegression(C=1.0, penalty='l2')
train_and_test(classifier, df, mlb)

# svm
from sklearn import svm
classifier = svm.SVC(probability=True, C=0.1)
train_and_test(classifier, df, mlb)

# xgboost(デフォルトパラメータだとrecallが0.3とかになったのでちょっと調整)
from xgboost import XGBClassifier
classifier = XGBClassifier(objective='binary:logistic', n_estimators=300, learning_rate=0.2)
train_and_test(classifier, df, mlb)

実行結果まとめ

model precision recall
logistic regression 0.8604 0.7346
svm 0.8170 0.7398
xgboost 0.8286 0.7196

precisionで0.8以上、recallで0.7以上はいけるらしい。

パラメータ調整等はほとんどしていないのでもう少し上がる余地はありそう。

最後の1〜2文字を別途特徴に加える

名前の男女の判定は主に後ろの1〜2文字が効いてくる。これは別途ベクトルをわけて特徴として扱った方が良いと思われる。

import pandas as pd

# ファイル読み込み
df = pd.read_csv('data/name_gender_estimator/name_gender.tsv', sep='\t')

# 読みとlabelだけにしてランダムソート
df = df[['yomi', 'label']].drop_duplicates()
df = df.sample(frac=1).reset_index(drop=True)

# bi-gramを取る(最後の1文字はend_○、最後の2文字はend_○○とする)
def bigram(text):
    return [text[i:i+2] for i in range(len(text) - 1)] + ['end_'+text[-2:], 'end_'+text[-1]]
df['bi_yomi'] = df.yomi.apply(bigram)

df.head(3)

実行結果

index yomi label bi_yomi
0 かなお 1 [かな, なお, endなお, endお]
1 みつひで 0 [みつ, つひ, ひで, endひで, endで]
2 てつや 0 [てつ, つや, endつや, endや]

これで再度学習を実行。

mlb = MultiLabelBinarizer()
mlb.fit(df.bi_yomi)

classifier = LogisticRegression(C=1.0, penalty='l2')
train_and_test(classifier, df, mlb)

classifier = svm.SVC(probability=True, C=0.1)
train_and_test(classifier, df, mlb)

classifier = XGBClassifier(objective='binary:logistic', n_estimators=300, learning_rate=0.2, max_depth=5)
train_and_test(classifier, df, mlb)

1が以前の結果。2が今回の結果。

model precision recall
logistic regression 1 0.8604 0.7346
logistic regression 2 0.8776 0.8022
svm 1 0.8170 0.7398
svm 2 0.8356 0.7864
xgboost 1 0.8286 0.7196
xgboost 2 0.8892 0.7974

全体的に向上した。

使われている漢字も特徴に入れてみる

利用されている漢字(特に末尾の子とか美とか)も利いてくると考えられるので、それも特徴に追加する。

import pandas as pd

# ファイル読み込み
df = pd.read_csv('data/name_gender_estimator/name_gender.tsv', sep='\t')

# 読みとlabel、あと漢字を各読みにつき最初のだけ選ぶ
df = df.groupby(['label', 'yomi'])['name'].first()
df = df.sample(frac=1).reset_index()

# bi-gramを取る(漢字も特徴に入れる)
def bigram(row):
    yomi = row.yomi
    yomi_features = [yomi[i:i+2] for i in range(len(yomi) - 1)] + ['end_'+yomi[-2:], 'end_'+yomi[-1]]
    kanji_features = list(row['name']) + ['end_' + row['name'][-1]]
    return yomi_features + kanji_features
df['bi_yomi'] = df[['name', 'yomi']].apply(bigram, axis=1)

df.head(3)

実行結果

index label yomi name bi_yomi
0 1 むろな 室奈 [むろ, ろな, endろな, endな, 室, 奈, end_奈]
1 0 こいちろう 虎一郎 [こい, いち, ちろ, ろう, endろう, endう, 虎, 一, 郎, end_郎]
2 0 よしろう 義郎 [よし, しろ, ろう, endろう, endう, 義, 郎, end_郎]

この特徴を使って学習。

mlb = MultiLabelBinarizer()
mlb.fit(df.bi_yomi)

classifier = LogisticRegression(C=1.0, penalty='l2')
train_and_test(classifier, df, mlb)

classifier = svm.SVC(probability=True, C=0.1)
train_and_test(classifier, df, mlb)

classifier = XGBClassifier(objective='binary:logistic', n_estimators=300, learning_rate=0.2, max_depth=5)
train_and_test(classifier, df, mlb)

実行結果。3が今回。

model precision recall
logistic regression 1 0.8604 0.7346
logistic regression 2 0.8776 0.8022
logistic regression 3 0.9138 0.8654
svm 1 0.8170 0.7398
svm 2 0.8356 0.7864
svm 3 0.9046 0.8488
xgboost 1 0.8286 0.7196
xgboost 2 0.8892 0.7974
xgboost 3 0.9180 0.8648

簡易なコードでprecision0.9まではいけた。頑張ればもうちょいいけそう。「はるき」や「ちひろ」等、どちらでもありえる名前もあるのでどこかで頭打ちにはなるはずだが。

判定ができてない名前たち

logistic regressionで判定ミスをした名前の一例。

name label predict proba
陽幸(ひゆき) 0.732680
滋(しげる) 0.861039
綴(つづり) 0.614519
楓(かえで) 0.746312
緑(みどり) 0.604013
真澄(ますみ) 0.712742

人の目で見ても間違えそうな名前が多い。

続いて判定できなかった(probabilityがthreshold以下だった)名前の一例。

name label proba
蕾(つぼみ) 0.504569
千夜(ちや) 0.481696
一花(いちか) 0.472305
和実(かずさね) 0.549066
還(めぐる) 0.568072
左近(さこん) 0.472167

学習データに類似のパターンが少ないケースや男女どちらでもありそうな名前が多く含まれる。

logistic regressionのモデルのcoefについても確認しておく。どんな特徴がスコアが大きくなっているか。

coefs = pd.DataFrame(index=range(len(mlb.classes_)))
coefs['feature'] = mlb.classes_
coefs['coef'] = classifier.coef_[0]
coefs.sort_values('coef', inplace=True)

# 男性を示すパラメータ
print(coefs.head(10))

# 女性を示すパラメータ
print(coefs.tail(10))

実行結果(男性)

feature coef
よし -1.740876
end_真 -1.617622
せん -1.242510
end_し -1.220412
あつ -1.196107
end_太 -1.170993
まさ -1.167951
みな -1.164397
end_治 -1.162645
end_樹 -1.143933

「よし」や「みな」が上位に入っているけど女性にも入る言葉だしあまり良い捉え方ではないような。その他は納得できるスコア。

実行結果(女性)

feature coef
end_美 1.606708
1.618670
end_子 1.638226
1.638226
end_え 1.667086
end_代 1.707443
1.771753
1.839155
1.911238
1.927380

こちらは女性らしい特徴を捉えていてなかなかに良い結果。

改定履歴

Author: Masato Watanabe, Date: 2019-2-19, 記事投稿