初心者でも機械学習で「英単語もどき」は生成できるか？～英単語判定機～

###はじめに
Google Colaboratoryとkerasを使って初歩的な機械学習をやってみた結果の記録です。
筆者自身が機械学習の初心者なので、ところどころおかしな部分や間違っている部分があるかもしれません。
初投稿なので至らない点があるかもしれませんがあらかじめご容赦ください。
（間違い、読みづらい部分の指摘等大歓迎です）

####想定する読者像
コピペレベルの機械学習はやったことがある人。
MNISTなどを終えて、今度は自分でデータセットを用意する簡単な機械学習をやってみたい人。
####前提条件
Googleアカウントを持っていて、Google Colaboratoryを使ったことがあること。
####今回の目標
自分でデータセットを作るところから機械学習をやってみること。（結果や精度は問わない） 
####やること
５文字のアルファベット文字列を見て、それが「存在する英単語」なのか、それとも「ランダムな文字列」なのかを判別するモデルを作る。
さらにそのモデルを使って、「英単語にありそうな文字列（英単語もどき）」を生成する。
***
###データを用意する
英語版wikiや英単語サイトなど、使えそうなのをどんどん引っ張ってきましょう。
これをメモ帳か何かにコピーし、そのtxtファイルをgoogledrive内にアップロードします。
メモ帳の置換機能を使って、５文字の英単語をいくつかデータから削除しておくのがポイントです（後でテストデータとして使います）。
ここでは、MyDrive内のMLフォルダのtrainフォルダ内に、train.txtとして保存しました。

###GoogleDriveをインポート
```Python:GoogleDriveをインポート
from google.colab import drive
drive.mount('/content/drive')
```
このコードを実行するとリンクが出てくるので、そこから自分のgoogleアカウントを選択してドライブをインポートします。
詳しいやり方は調べればすぐに出てきます。

```Python:データの読み込み
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、というようにアルファベットと数値を対応させます

```Python:アルファベットと数値の変換
char="abcdefghijklmnopqrstuvwxyz"
dic={}
for i in range(len(char)):
  dic[char[i]]=i
print(dic['c'])#cは3番目なので、2が出力されるはず。（リストの番号は0,1,2...なので）
```

###訓練データ作成

読み込んだ英文データから、５文字の英単語だけを抜き出していきます。

```Python:「存在する英単語」データの作成
import re
def correct_generate(rawdata,data_size=10000,length=5):#lengthは生成する単語の長さです。今回は５文字に設定しています。
  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)#長さ５文字の単語だけを集めています
  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]]
```




続いて、乱数を用いてランダムな５文字の文字列を生成します。個数は上で生成した「存在する英単語」の数と似たような感じにしましょう。

```Python:「ランダムな文字列」データの生成
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()
```

上で作成した２つのデータをくっつけて、訓練データにします。
y_train_listは正解ラベルです。listsがランダムな文字列、betaが存在する英単語なので、それぞれの要素数に対応する数だけ0,1を並べます。ここでは0が「ランダムな文字列である」、1が「存在する英単語である」を表します。もっと正解ラベルのバリエーションを増やしたいとき（例：MNISTの手書き数字判別）は、y_train_listに2,3,4...と入れるようにすればOKです。

```Python:訓練データの元を作成
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エンコーディングです。


```Python: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に変換します。

```Python:listを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の部分は「存在する英単語」と「ランダムな文字列」のデータの総数です。
これで訓練データができました！

###学習モデルの作成
```Python:モデル
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です。いろいろ試してください。

```Python:コンパイル
model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
```
###学習
学習させます。今回はデータが軽いので10秒ほどで終わります。

```Python:学習
history = model.fit(x_train, y_train, batch_size=128, epochs=40,
                    validation_split=0.1,shuffle=True )
```

今回はデータを学習させる順序をバラバラにするため、shuffle=Trueとしました。（必要かはよくわかりません）
validation_splitはデータの何割を訓練ではなく評価に使うか、みたいなことです。
epochsはある程度大きくないと学習が収束しないまま終わりますが、大きければ大きいほど良いかというとそういうわけではないので、
ある程度学習が収束するあたりで止めておくのがいいでしょう。

###テスト
特に必須というわけではありませんが、元データにその単語が入っているかどうかを確認する関数を作っておきましょう。

```Python:データの確認
def nothing_val(word):
  for w in alpha:
    if word==w:
      return False
  return True
nothing_val('beast')
```
続いて、単語とベクトルを相互に変換する関数を作ります。

```Python:単語とベクトルの変換
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]
```

続いて、存在するが訓練データに含めていない単語、およびランダムな文字列を使ってテストを行います。
（ここでデータ作成時にわざと取り除いた単語を使います）

```Python:テスト
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...であると判断しているというわけです。かなり高い精度で判断できていることが分かりました。

###「英単語もどき」の生成

続いて、大量のランダム文字列を生成し、それぞれが「存在する英単語」である確率を求めてその確率が大きい順（逆に言えば、「ランダムな文字列」である確率が小さい順）に並び替え、その先頭に来る文字列をいくつか取ってくれば、英単語っぽいランダム文字列「英単語もどき」が得られるのでは？ということです。ではやっていきましょう。


```Python:ランダム文字列の大量生成、並べ替え
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])#２つ目のデータ（「ランダム文字列である確率」）でソート
```
このコード、実は大量に文字列を生成しているせいで処理に１分半くらいかかります。少しお待ちください。  
最後に、予測結果の「存在する単語」である確率が最も大きい30単語をその確率とともに表示させます。


```Python:「英単語もどき」の生成
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