概要
名前から性別を判定するのって簡単にできるのかな、ということで簡易な判定器を書いて試してみる。
バージョン情報
- 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, 記事投稿