はじめに
Google Colaboratoryとkerasを使って初歩的な機械学習をやってみた結果の記録です。
筆者自身が機械学習の初心者なので、ところどころおかしな部分や間違っている部分があるかもしれません。
初投稿なので至らない点があるかもしれませんがあらかじめご容赦ください。
(間違い、読みづらい部分の指摘等大歓迎です)
想定する読者像
コピペレベルの機械学習はやったことがある人。
MNISTなどを終えて、今度は自分でデータセットを用意する簡単な機械学習をやってみたい人。
前提条件
Googleアカウントを持っていて、Google Colaboratoryを使ったことがあること。
今回の目標
自分でデータセットを作るところから機械学習をやってみること。(結果や精度は問わない)
やること
5文字のアルファベット文字列を見て、それが「存在する英単語」なのか、それとも「ランダムな文字列」なのかを判別するモデルを作る。
さらにそのモデルを使って、「英単語にありそうな文字列(英単語もどき)」を生成する。
データを用意する
英語版wikiや英単語サイトなど、使えそうなのをどんどん引っ張ってきましょう。
これをメモ帳か何かにコピーし、そのtxtファイルをgoogledrive内にアップロードします。
メモ帳の置換機能を使って、5文字の英単語をいくつかデータから削除しておくのがポイントです(後でテストデータとして使います)。
ここでは、MyDrive内のMLフォルダのtrainフォルダ内に、train.txtとして保存しました。
GoogleDriveをインポート
from google.colab import drive
drive.mount('/content/drive')
このコードを実行するとリンクが出てくるので、そこから自分のgoogleアカウントを選択してドライブをインポートします。
詳しいやり方は調べればすぐに出てきます。
base='drive/MyDrive/ML/train/'#train.txtを配置したフォルダを指定します。
f = open(base+'train.txt', 'r')
rawdata = f.read()
data=rawdata.split()
print(data[:5])
f.close()
['This', 'is', 'a', 'pen', '.']
下準備
aは0、bは1、というようにアルファベットと数値を対応させます
char="abcdefghijklmnopqrstuvwxyz"
dic={}
for i in range(len(char)):
dic[char[i]]=i
print(dic['c'])#cは3番目なので、2が出力されるはず。(リストの番号は0,1,2...なので)
訓練データ作成
読み込んだ英文データから、5文字の英単語だけを抜き出していきます。
import re
def correct_generate(rawdata,data_size=10000,length=5):#lengthは生成する単語の長さです。今回は5文字に設定しています。
alpha_data=[]
for word in rawdata:
word=word.lower()
res = re.sub(r'[^a-zA-Z]', '', word)#正規表現を使って、数字や記号などアルファベット以外を''に置換(つまり消去)している
if len(res)==length:
alpha_data.append(res)#長さ5文字の単語だけを集めています
set_alpha=list(set(alpha_data))#setを使って重複を取り除き、listに戻します
beta_data=[]
for word in set_alpha:
num=[]
for id in range(len(word)):
c=word[id]
num.append(dic[c])
beta_data.append(num)#単語に対応する数字を入れていきます。例:"apple"->[0,15,15,11,4]
size=len(beta_data)
return set_alpha, beta_data, size
_,beta,size=correct_generate(data)
print(size)
print(beta[:5])
961
[[2, 0, 19, 2, 7], [3, 17, 14, 22, 13], [2, 11, 14, 18, 4], [22, 0, 19, 2, 7], [1, 17, 8, 13, 6]]
続いて、乱数を用いてランダムな5文字の文字列を生成します。個数は上で生成した「存在する英単語」の数と似たような感じにしましょう。
import random
def generate(data_size=1000,length=5):
random_list=[]
random_words=[]
for i in range(data_size):
word=''
num=[]
for j in range(length):
a=random.randrange(26)
c=char[a]
word+=c
num.append(a)
random_list.append(num)
random_words.append(word)
return random_list,random_words
lists,word=generate()
上で作成した2つのデータをくっつけて、訓練データにします。
y_train_listは正解ラベルです。listsがランダムな文字列、betaが存在する英単語なので、それぞれの要素数に対応する数だけ0,1を並べます。ここでは0が「ランダムな文字列である」、1が「存在する英単語である」を表します。もっと正解ラベルのバリエーションを増やしたいとき(例:MNISTの手書き数字判別)は、y_train_listに2,3,4...と入れるようにすればOKです。
x_train_rawlist=lists+beta
y_train_list=[]
for i in range(len(lists)):
y_train_list.append(0)
for i in range(len(beta)):
y_train_list.append(1)
print(x_train_rawlist[0])
x_train_rawlistは、[[3, 17, 14, 22, 13], [2, 11, 14, 18, 4]]みたいな感じです。
しかしここで、aは0、bは1、zは26ですが、aとbの数字が近く、aとzの数字が遠いことに本質的な意味はありません。
このまま学習させると、"apple"がいいなら"bpplf"もいいんじゃない?みたいな判定の仕方をするモデルができてしまいます。
それはまずいので、各文字に対応させる3や24のような数字を、[0,0,0,1,0,...0,0,0]や[0,0,0,0,0,...0,1,0]のような26次元ベクトルに変換します。この操作がone_hotエンコーディングです。
import numpy as np
x_train_list=[]
for data in x_train_rawlist:
item=[]
for num in data:
one_hot=np.zeros(26)#26次元零ベクトルの生成
one_hot[num]=1
one_hot_list=one_hot.tolist()
item.append(one_hot_list)
x_train_list.append(item)
kerasを使うため、データをnumpyに変換します。
x_train=np.array(x_train_list)
y_train=np.array(y_train_list)
print(x_train.shape)
print(y_train.shape)
(1961,5,26)
(1961,)
1961の部分は「存在する英単語」と「ランダムな文字列」のデータの総数です。
これで訓練データができました!
学習モデルの作成
import tensorflow as tf
from tensorflow.keras.models import Sequential
model = tf.keras.Sequential(name='my_model')
model.add(tf.keras.layers.Flatten(input_shape=(5,26), name='flatten_layer_1'))
model.add(tf.keras.layers.Dense(128, activation='relu'))
model.add(tf.keras.layers.Dropout(0.2))
model.add(tf.keras.layers.Dense(256, activation='relu'))
model.add(tf.keras.layers.Dropout(0.2))
model.add(tf.keras.layers.Dense(512, activation='relu'))
model.add(tf.keras.layers.Dropout(0.2))
model.add(tf.keras.layers.Dense(256, activation='relu'))
model.add(tf.keras.layers.Dropout(0.1))
model.add(tf.keras.layers.Dense(128, activation='relu'))
model.add(tf.keras.layers.Dropout(0.1))
model.add(tf.keras.layers.Dense(2, activation='softmax'))
model.summary()
4行目のinput_shape=(5,26)はx_train.shapeに、最終行のDenseの引数「2」はラベルの数にそれぞれ対応しています。
逆に言えば他の数は多少いじっても多分大丈夫です。
詳しいことは「ゼロから作るDeepLearning」を読むとよく分かります。
適宜レイヤーの数を増やしてみたりノード数を変えてみたりしてください。
モデルをコンパイルします。optimizerは'sgd'とかでもOKです。いろいろ試してください。
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
学習
学習させます。今回はデータが軽いので10秒ほどで終わります。
history = model.fit(x_train, y_train, batch_size=128, epochs=40,
validation_split=0.1,shuffle=True )
今回はデータを学習させる順序をバラバラにするため、shuffle=Trueとしました。(必要かはよくわかりません)
validation_splitはデータの何割を訓練ではなく評価に使うか、みたいなことです。
epochsはある程度大きくないと学習が収束しないまま終わりますが、大きければ大きいほど良いかというとそういうわけではないので、
ある程度学習が収束するあたりで止めておくのがいいでしょう。
テスト
特に必須というわけではありませんが、元データにその単語が入っているかどうかを確認する関数を作っておきましょう。
def nothing_val(word):
for w in alpha:
if word==w:
return False
return True
nothing_val('beast')
続いて、単語とベクトルを相互に変換する関数を作ります。
def word2num(word):
num=[]
for id in range(len(word)):
num.append(dic[word[id]])
return num
def num2word(nums):
word=''
for i in nums:
word+=char[i]
return word
print(num2word([1,4, 0, 18, 19]))
print(word2num("beast"))
beast
[1, 4, 0, 18, 19]
続いて、存在するが訓練データに含めていない単語、およびランダムな文字列を使ってテストを行います。
(ここでデータ作成時にわざと取り除いた単語を使います)
test_words=["beast","blade","joker","kdjus","adjsa"]
y_test=[1,1,1,0,0]
x_test=[]
for word in test_words:
vec=word2num(word)
item=[]
for num in vec:
data=np.zeros(26,dtype=int)
data[num]=1
data_list=data.tolist()
item.append(data_list)
x_test.append(item)
test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)
print(test_loss)
print(test_acc)
predictions = model.predict(x_test)
print(predictions)
0.000818554952275008
1.0
[[1.9652704e-09 1.0000000e+00]
[1.7644927e-08 1.0000000e+00]
[4.0833335e-03 9.9591666e-01]
[1.0000000e+00 2.2184957e-11]
[9.9999893e-01 1.0198289e-06]]
predictionsはそれぞれのラベルである「確率」を表します。
たとえば、[[0.2,0.8]]なら、「ランダムな文字列」である確率が0.2、「存在する英単語」である確率が0.8ということです。
(「ランダムな文字列」にラベル0、「存在する英単語」にラベル1を割り当てているので)
例えば"joker"が「存在する英単語」である確率は0.99591666...であると判断しているというわけです。かなり高い精度で判断できていることが分かりました。
「英単語もどき」の生成
続いて、大量のランダム文字列を生成し、それぞれが「存在する英単語」である確率を求めてその確率が大きい順(逆に言えば、「ランダムな文字列」である確率が小さい順)に並び替え、その先頭に来る文字列をいくつか取ってくれば、英単語っぽいランダム文字列「英単語もどき」が得られるのでは?ということです。ではやっていきましょう。
length=5#文字列の長さ
randomlist,randomword=generate(data_size=100000)#10万個の文字列を生成
input=[]
for vec in randomlist:
item=[]
for num in vec:
data=np.zeros(26,dtype=int)#one_hotエンコーディング
data[num]=1
data_list=data.tolist()
item.append(data_list)
input.append(item)
r_pred=model_1.predict(input)#モデルを用いて予測
item=[]
for i in range(len(r_pred)):
res=[i,r_pred[i][0]]#データのidと「ランダム文字列である確率」を組として持つ
item.append(res)
item.sort(key=lambda item:item[1])#2つ目のデータ(「ランダム文字列である確率」)でソート
このコード、実は大量に文字列を生成しているせいで処理に1分半くらいかかります。少しお待ちください。
最後に、予測結果の「存在する単語」である確率が最も大きい30単語をその確率とともに表示させます。
top=30
ans=[]
for i in range(top):
id=item[i][0]
ans.append([randomword[id],item[i][1]])
print([randomword[id],item[i][1]])
['aresn', 8.140479e-12]
['fross', 9.360529e-11]
['srirm', 1.4889086e-10]
['siesd', 2.8570077e-10]
['suand', 3.077406e-10]
['sramn', 3.6880232e-10]
['slosn', 3.9755757e-10]
['coias', 6.4331496e-10]
['bresn', 6.9942746e-10]
['spise', 7.33037e-10]
['suyse', 9.483382e-10]
['sxaae', 9.978939e-10]
['aposd', 1.1661512e-09]
['sroks', 1.2433329e-09]
['auese', 1.3839621e-09]
['sracd', 1.4601056e-09]
['srund', 1.9346504e-09]
['crobs', 2.1252802e-09]
['siiil', 2.2484026e-09]
['aliad', 3.0197276e-09]
['siind', 3.1254523e-09]
['sioas', 3.6240808e-09]
['arasc', 3.9477634e-09]
['aoasm', 5.764397e-09]
['criry', 6.511828e-09]
['srimk', 6.735973e-09]
['adind', 6.998508e-09]
['shabt', 7.3702595e-09]
['wuory', 7.948066e-09]
['seacl', 8.938689e-09]
どうでしょう? "fross","criry"など、言われてみれば英単語にありそう...?な文字列が並んでいることが分かります。
("criry"は固有名詞としてあるみたいですね。元データには入っていなかったので、この機械学習で見つけ出した英単語といってよいでしょう。)
ということで、kerasを用いた機械学習をやってみました。精度はまちまちだったかもしれませんが、ともかく今回の目標の「自分でデータセットを作るところから機械学習をやってみる」は達成できたかと思います。
同じような発想でいろいろなモデルが作れると思いますので、ぜひ作ってみてください。
↓コードを載せています
https://github.com/mirrormouse/machine_learning/blob/main/english_words_backup.ipynb