人工知能に「ニュアンス」を伝えたい!

こんにちは!aidemyの研修生のたかおです!

突然ですが!みなさんは、誰かの名前を聞いたときにまず女性か男性かが頭に思い浮かびませんか?あれってよく考えたら不思議だと思います。
「子」で終わる名前は女性?でもそんな単純じゃないはず。。。

答えは簡単です!それは、人は言葉にニュアンスを感じているのです!
名前を聞いた時のニュアンスで女性か男性かを判断しているんですね。人はたくさんの人の名前と性別を見聞きする経験によってこのニュアンスを得ているのです。

ここでまた新たな疑問が生まれました。
人工知能にこの主観的なニュアンスを伝えることができるのか!?

つまりまとめるとこんな感じです!

どうして名前を聞いたら性別が分かるの??

人は名前にニュアンスを感じるからだよ。

人工知能にニュアンスを伝えることはできるの??

人工知能が名前から性別を判別できればニュアンスを伝えれたということ!

ということで今回は、人工知能に名前と性別を学習させて性別判定機を作ります!
では、さっそく開発していきましょう!

開発環境

今回は、Google Colaboratory を利用しました! Google社のGPU,TPUを利用できる有り難い代物です。

  • OS : ubuntu 17.10
  • CPU : Intel(R) Xeon(R) CPU @ 2.30GHz
  • GPU : Tesla K80
  • メモリ : 12GB
  • プログラミング言語 : python

今回の開発で使用したライブラリはこちらです!

# 必要なモジュールをインポートする
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from keras.utils.np_utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout
from keras.layers.recurrent import SimpleRNN
from keras.layers.normalization import BatchNormalization
from keras.callbacks import EarlyStopping
from keras.optimizers import Adam
from sklearn.model_selection import train_test_split
from bs4 import BeautifulSoup
import requests
import re
import numpy as np

データの取得

まずはデータ集めから始めていきましょう!
方法としては、スクレイピングを行っていきます。スクレイピングとは、webサイトなどからデータを抽出することです。機械学習をするなら必須のスキルとなってくるのでぜひ習得しましょう!

男性の名前(ひらがな)5000個、女性の名前(ひらがな)5000個のデータを集めていきます!
今回は、以下のサイトを利用しました。

お名前辞典

スクレイピングにはBeautifulSoupを利用しました!
以下がスクレイピングのプログラムです!

プログラム

#スクレイピング先のurl
url = "http://name.m3q.jp/ranking?g=1" #男性
#url = "http://name.m3q.jp/ranking?g=2" #女性

name_list = []
for i in range(100):
    res = requests.get(url)
    res.encoding = res.apparent_encoding
    soup = BeautifulSoup(res.text,"html.parser")
    
    names = soup.find("div",class_="tags-list").find_all("a")

    for name in names:
        name_list.append(name.text + "\n")
    
    #次ページのurlを取得
    next_url = soup.find("a",class_="flat-button page-nav-next")
    url = next_url.get("href") 
    
#google driveにテキストファイルとして保存
with open("./boy_name.txt", mode='w') as f:
    f.writelines(name_list)

1つのページから50個の名前データを取得することができます!
男性名、女性名、それぞれ100ページ分(合計10,000個)のデータを取得し、テキスト形式として保存します。

データの加工

さて、必要なデータは集まりましたが、このままの形では学習に使えません。
不要なデータを削除したり、適当な形に変形していきます!

不要な文字の削除

取得したデータには不要な空白や文字が含まれています。
以下のプログラムを使って、それを削除していきます!

プログラム

#ファイルを一行読み込み
str_list = []
with open("./girl_name.txt") as f:
    for s_line in f:
        str_list.append(s_line) 

for i, mystr in enumerate(str_list):
    #数字の削除
    str_list[i] = re.sub("[0-9]","",str_list[i])
    #空白の削除
    str_list[i] = re.sub(" ","",str_list[i])
 
#改行のみの要素,被っている要素を削除
str_list = [i for i in str_list if i != "\n"]
str_list = [x for x in str_list if x] 

これできれいな名前だけのデータに整形することが出来ました!

文字のベクトル表現

きれいなデータが用意できたので早速学習!といきたいところですが、今回は深層学習を扱うので文字のままだと学習できません。そこで、文字を深層学習に対応したベクトル表現に変えていきます!

 One-hot表現

ベクトル化の手法には、One-hot表現やBOW表現などがあります!
BOW表現は単純で、単語内にその文字がどのくらいの頻度で含まれているかに着目したベクトルです。しかし今回は、文字の順番に注目したモデルを作っていこうと思っているので、頻度に注目したBOW表現は扱いません。

そして、今回の開発ではOne-hot表現を扱いたいと思います。
One-hot表現というのはある要素のみが1でその他の要素が0であるような表現方法のことです。各次元に 1 か 0 を設定することで「その文字か否か」を表します。

後に紹介するRNNモデルとOne-hot表現を利用することで、文字の順番を考慮したモデルを作っていきます!
以下のプログラムでベクトル化をしていきます!

 プログラム

#すべてのひらがなを含む文字列を生成
all_letters = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをんがぎぐげござじずづぜぞだぢでどはびぶべぼゃゅょ"
n_letters = len(all_letters)

category = ["男性","女性"]
n_category = len(category)

#データの読み込み
girl_data = pd.read_csv("girl_name.txt", sep="\n", header=None)
boy_data = pd.read_csv("boy_name.txt", sep="\n", header=None)

name_list = []
label_list = []
max_len=0

for name in girl_data[0]:
    name_list.append(name)
    label_list.append(0)
    if len(name) > max_len:
        max_len = len(name)
    
for name in boy_data[0]:
    name_list.append(name)
    label_list.append(1)
    if len(name) > max_len:
        max_len = len(name)

# カテゴリ予測なので、正解ラベルを0,1のバイナリデータの配列に変換する
label_list = to_categorical(label_list, n_category)

#one-hot表現に変換
# 文字のインデックスを返す "あ"= 0
def letterToIndex(letter):
    return all_letters.find(letter)

# 名前をone-hot表現でベクトルに変換
def lineToTensor(line):
    tensor = np.zeros([max_len,n_letters],dtype=int)
    for li, letter in enumerate(line):
        tensor[li][letterToIndex(letter)] = 1
    return tensor

#名前を変換
data_list=[]
for name in name_list:
    data_list.append(lineToTensor(name))
data_list = np.array(data_list)

letterToIndex()関数は該当文字のインデックスを返す関数です。lineToTensor()関数は letterToIndex()関数を利用して文字列をOne-hot表現に変換しテンソルとして返してくれます。
One-hot表現に変換したデータはdata_listに、ラベルデータはバイナリデータに変換してlabel_listに保管します。
これでデータの準備が整いました!

RNN

今回の開発では、学習モデルとしてRNNを選択しました。ざっくりですが、RNNの概要を説明していきます!

RNNの概要

RNN(Recurrent Neural Network)とは、ニューラルネットワークに時間の概念を加えたものです!
RNNのR(Recurrent)は再帰的構造を持つという意味です。言い換えれば、出力が入力に影響を与えるということになります!
データをループさせることがRNNのネットワークの特徴であり、ループをするメリットは、「過去の情報を記憶しながら絶えず最新のデータを持ち続けられる」ことです。
RNNでは時間の概念をニューラルネットワークに取り入れるために、過去の情報をモデル内で保持しています。

このような仕組みが、前後の文脈を保持したまま情報の伝達・解釈を行うことを可能にしています。
下の図はRNNの簡単な流れを表した図になります。

今回扱う名前データは、連続した文字を連結したものです。
RNNを用いることで、文字の順番という文脈を考慮して学習を行うことができます!

学習モデルの構築

それでは実際に学習モデルを構築していきます!
学習モデルの構築にはkerasを用いて設計します。kerasはpythonで書かれた深層学習用のライブラリで、直感的なコーディングをすることができ、初学者に扱いやすい設計となっています。
以下がモデルのプログラムになります!

プログラム

def pred_activity_rnn(input_dim,
                       activate_method='softmax',  # 活性化関数
                       loss_method='categorical_crossentropy',  # 損失関数
                       optimizer_method=Adam(lr=0.00005, beta_1=0.9, beta_2=0.999),  # パラメータの更新方法
                       kernel_init_method='glorot_normal',  # 重みの初期化方法
                       batch_normalization=False,  # バッチ正規化
                       dropout_rate=0.5  # ドロップアウト率
                       ):
    # レイヤーを定義
    model = Sequential()
    model.add(
        SimpleRNN(
            input_shape=(input_dim[0], input_dim[1]),
            units=60,
            kernel_initializer=kernel_init_method,
            return_sequences=True  # さらにRNNレイヤーを重ねるのであればTrueにする
        ))

    # バッチごとに正規化を行う
    if batch_normalization:
        model.add(BatchNormalization())

    # ドロップアウトにより、ニューロンをランダムに削除する
    # pred_activity_rnn(dropout_rate=0.5)ならドロップレートを50%で設定する
    if dropout_rate:
        model.add(Dropout(dropout_rate))

    model.add(
        SimpleRNN(
            units=30,
            kernel_initializer=kernel_init_method,
            return_sequences=False  # この後にRNNレイヤーはないのでFalseにする
        ))

    # バッチごとに正規化を行う
    if batch_normalization:
        model.add(BatchNormalization())

    # ドロップアウトにより、ニューロンをランダムに削除する
    if dropout_rate:
        model.add(Dropout(dropout_rate))

    model.add(Dense(units=n_category, activation=activate_method))
    model.compile(loss=loss_method, optimizer=optimizer_method,
                  metrics=['accuracy'])

    return model

# モデルの作成
input_size = [data_list.shape[1], data_list.shape[2]]
turned_model = pred_activity_rnn(
    input_dim=input_size,
    activate_method='softmax',
    loss_method='categorical_crossentropy',
    optimizer_method='adamax',
    kernel_init_method='glorot_normal',
    batch_normalization=False
)

損失関数にはcategorical_crossentropyを、optimizerにはadamを使用しています。
モデルを定義する際に、関数型にするとチューニングが分かりやすくなって良いです!

結果

それでは実際に学習を始めていきましょう!

X_train,X_test,y_train,y_test =
 train_test_split(data_list, label_list, test_size=0.3,random_state=0)

まず、学習用データ、教師ラベルを、トレーニングデータ、テストデータに分割します!
X_train, y_trainは学習用データ、X_test, y_testは教師ラベルとなります。
今回はトレーニングデータ:テストデータを7:3で分割します。

# early_stoppingの定義
early_stopping = EarlyStopping(monitor='val_loss', patience=10, verbose=1)

# 学習スタート
history = turned_model.fit(
    X_train,
    y_train,
    batch_size=100,
    epochs=20,
    validation_split=0.3,
    callbacks=None,
    verbose=2
)

バッチサイズは100で100エポック学習させます。

# 精度の推移図を出力
plt.figure(figsize=(8, 5))
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.xticks(np.arange(0, 100 + 1, 10))
plt.title('model accuracy')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()

# 損失関数の推移図を出力
plt.figure(figsize=(8, 5))
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xticks(np.arange(0, 100 + 1, 10))
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'test'], loc='upper left')
plt.show()
y_pred = turned_model.predict(X_test, batch_size=100, verbose=0)
score = turned_model.evaluate(X_test, y_test, verbose=0)
print('Test loss:', score[0])
print('Test accuracy:', score[1])

上のプログラムを実行した結果がいかになります!

Test loss: 0.4411894009242079
Test accuracy: 0.8050989601005047

精度は80%とまずまずですね!
上の図は精度の、下の図は損失関数の推移を示したものです。30エポックほどでテスト時はほぼ横ばいですね。これ以上行くと過学習気味になるので100エポックでストップです。

テストデータを使った検証

つぎに、いくつかのテストデータを使ってどのように判定がされているか見てみようと思います!

category = ["女性","男性"]
test_name = ["あいこ","かな","りか","めい","たつき","りょう","たかお","るん","あくわ","ちふす","れう"]
test_label = ["女性","女性","女性","女性","男性","男性","男性","女性","男性","女性","女性"]
test_vec = []

for name in test_name:
    test_vec.append(lineToTensor(name))
test_vec = np.array(test_vec)
    
result = pd.DataFrame()
result["名前"] = test_name 
y_pred = turned_model.predict(test_vec)
y_pred = [category[np.argmax(np.array(x))] for x in y_pred]
result["判定"] = y_pred
result["正解"] = test_label
result

上のプログラムを実行した結果が以下になります!

テストデータとして、女性的な名前4個、男性的な名前4個と中性的な名前を4個を用意しました。男性的、女性的な名前に関しては正答率が100%ですが、中性的な名前に関しては、4個中2個誤判定となっています。

ひらがな → アルファベットに変換してチャレンジ!

こんどは、ひらがなをアルファベットに変換して挑戦してみます。
アルファベットに変換することで、1文字を母音+子音に分解できるので、モデルの表現度が上がるのではないかと考えました!

プログラム

#すべてのalphabetを含む文字列を生成
all_letters = "abcdefghijklmnopqrstuvwxyz"

#ひらがな→アルファベットに変換
from pykakasi import kakasi

name_list = []
label_list = []

kakasi = kakasi()
kakasi.setMode("H", "a")

conv = kakasi.getConverter()
max_len=0

for name in girl_data[0]:
    alpha = conv.do(name)
    name_list.append(alpha)
    label_list.append(0)  
    if len(alpha) > max_len:
        max_len = len(alpha)
       
for name in boy_data[0]:
    alpha = conv.do(name)
    name_list.append(alpha) 
    label_list.append(1)
    if len(alpha) > max_len:
        max_len = len(alpha)

ひらがな → アルファベットへの変換にはpykakasiを利用しました。pykakasiは、日本語をローマ字に変換するモジュールです。
まず、pykakasiのkakasi()というインスタンスを作成します。
次に、kakasi.setMode(“H”, “a”)でひらがな→アルファベットに変換する設定をしています。そして、kakasi.getConverter()インスタンスを作成し、conv.do(name)でnameに入っているひらがな文字列をアルファベットに変換しています。

pykakasiは標準ライブラリではないため、実行する際はいかのコマンドを用いてインストールしてください!

!pip install pykakasi

結果

それでは結果を見ていきましょう!

Test loss: 0.484358627357886
Test accuracy: 0.7742368333174133

精度は77%と下がってしまいましたね。。。
アルファベットはひらがなに比べて少ないため、モデルの表現度が下がってしまったのかもしれません。

考察

[精度が上がらない原因] 名前の多様性が高くなっている!?

精度が80%どまりで向上しなかった理由を考察したいと思います。
理由の1つとして考えられることは、現代において、名前の多様性が高くなっているということです。今回スクレイピングに用いたwebページの名前一覧から、いくつか例をあげましょう。「そら」、「るか」といったように昔は女性にしか見られなかった名前が男性にあったり、「ぽっぺんぱいん」(男性名)、「めいふぁ」(女性名)といったような、中性的な名前も多くありました(20代の自分から見て)。
このように、現代における名前は多様化していて、人工知能も判別率が下がったと考えました。

今後の展望

[提案] 構成要素間の相互作用を考慮

今回の開発では、名前の構成要素を文字(ひらがなとアルファベット)として扱ってきました。
そして、RNNをモデルとして採用することにより、文字の順番を考慮しました。しかし、人間の印象のような主観的な事象を扱うには単純すぎる気がします。
そこで、名前の構成要素間の相互作用を考慮することを考えました。
例えば、一音目の母音が「あ段」で三音目の子音が「か行」の場合の相互作用といったような感じです。これを変数化することができればより表現力のあるモデルが構築できそうだと考えました!
具体的な手法についてはまた考えていければいいなと思っています!

結果的に、まあまあニュアンスを伝えられたんじゃないでしょうか!?
それではまた次の記事でお会いしましょう。最後までご覧くださりありがとうございました。

プログラミング未経験からでもAIスキルが身につくAidemy Premium




PythonやAIプログラミングを学ぶなら、オンライン制スクールのAidemy Premiumがおすすめです。
「機械学習・ディープラーニングに興味がある」
「AIをどのように活用するのだろう?」
「文系の私でもプログラミング学習を続けられるだろうか?」
少しでも気になることがございましたら、ぜひお気軽にAidemy Premiumの【オンライン無料相談会】にご参加いただき、お悩みをお聞かせください!