機械学習モデルで「東京-関東+関西=?」の計算をしてみた(自然言語処理)
0. 概要
はじめに
Python初心者が機械学習のモデル制作に挑戦してみました。
今回は日本語を学習させて、
「東京 - 関東 + 関西 = 大阪」
のような、日本語の算術ができるプログラムを作ってみました。
使用する機械学習モデルなど
・機械学習モデル
Word2Vecという機械学習のモデルを使用します。
文章の中にある単語を数百次元のベクトルに変換することで、
'king' - 'man' + 'woman' = 'queen'
といった単語間の算術を可能にします。
このモデルの非常に強力で面白い特性といえます。
・学習データ
学習データには日本語Wikipediaのダンプ(記事データ)を使いました。
今回のプログラムのプロトタイプを作った時は、
入手の簡単な青空文庫やlivedoorコーパスを使用しましたが、
ジャンルやトピックの偏り、データの少なさが大きく結果に影響し、
満足な結果が得られませんでした。
その反省から、偏りがなく、データも多いWikipediaを選択しました。
・形態素解析器
文章を、モデルに学習可能な型にするために形態素解析器が必要ですが、
日本語の形態素解析器では、MeCabとJanomeがよく使われます。
Janomeはインストールは簡単ですが、処理に時間がかかります。
インストールは手間がかかるが(辞書を別途入手する必要あり)、
処理の早いMeCabを今回は選択しました。
1. プログラム制作
1.1. 実行環境について
・Google Colaboratory
・python - 3.10.12
・gensim - 4.3.1
・neologdn-0.5.1
・mecab-python3 - 1.0.6
・unidic-lite-1.0.8
・wikiextractor-3.0.6
1.2. 準備i:インストールするパッケージ
※注:Google Colaboratory上で実行することを想定しています。
##################################
#インストールするもの
##################################
!pip install gensim
!pip install mecab-python3
!pip install unidic-lite
!pip install wikiextractor
!pip install neologdn
gensim: 自然言語処理のためのオープンソースライブラリで、Word2Vecはこれに含まれています。
wikiextractor: Wikipediaのダンプ(全記事データ)からテキストを抽出するためのツールです。
neologdn: 日本語テキストの正規化を行うライブラリです。
mecab-python3: Pythonから形態素解析器MeCabを使用するためのラッパーライブラリです。
unidic-lite: MeCabで使用する辞書の一つで、より新しい言葉や表現もカバーしています。
1.3. 準備ii:wikipediaからデータ取得
日本語Wikipediaのダンプ(全記事データ)は下記のURLからダウンロードします。
https://dumps.wikimedia.org/jawiki/latest/
ページ内の'jawiki-latest-pages-articles.xml.bz2 'を選択してダウンロードします。
取得したデータはgzip圧縮データで、解凍すると
'jawiki-latest-pages-articles.xml'のxmlファイルが取得できます。
xmlファイルをテキストファイルに変換するのに、以下のコードを使います。
※注:Google Colaboratory上で実行することを想定しています。
##################################
#xmlファイル → テキストファイルの変換
##################################
!python -m wikiextractor.WikiExtractor -o <保存先のパス> <.xmlファイルのパス>
変換されたテキストファイルは、数百のファイルに分割されて保存されます。
<保存先のパス>内に'AA', 'AB', 'AC' ….とディレクトリが作られ、
1つのディレクトリには'wiki_00','wiki_01' 〜'wiki_99'までの100個のテキストデータが格納されます。
2023/6/24時点では、ディレクトリは'AA'〜'BK'の37フォルダでき、
総ファイル数は3692個でした。
'wiki_XX'の容量は1ファイルあたり約1MBです。
どのくらいの文章量なのか、試しに'AA'フォルダの'wiki_00'をワードで開いてみたところ、なんと313ページの分量でした!
先達の方々のブログを見ると、数千に分割された'wiki_XX'を1つのtxtファイルに統合して、学習用データに使っている例が少なからずあり、驚嘆します。
私も統合まではしてみましたが、約3.4GBの激重ファイルになり、扱うことが困難で断念しました。
2.1.失敗例では、'AA'ディレクトリ内の'wiki_00'で学習させています。
2.2.成功例では、'AA'ディレクトリ内の'wiki_00'〜'wiki_99'の
100個のファイルを統合させたデータで学習させています。
1.4. プログラム全文
さて、全文は以下の通りです。
##################################
# 1.必要なライブラリのインポート
##################################
from gensim.models import word2vec #Word2Vecのインポート
import MeCab #形態素解析に使う
import unidic_lite as unidic #MeCab用の辞書
import re #テキストデータのクリーニングに使う
import neologdn #テキストデータの正規表現化に使う
##################################
# 2.Wikipediaから取得したテキストデータをクリーニング、正規化する
##################################
input_file = "取得したWikipediaのテキストデータのパス"
with open(input_file,"r", encoding = "utf-8") as f:
text = f.read()
text = re.sub(r'<(/?)doc.*?>','',text) #元データのdocタグを取り除く
text = re.sub(r'\n\s*\n','\n',text) #同様に、空白行を取り除く
text = neologdn.normalize(text) #正規表現にする
#全テキストデータを改行単位で分割する(処理落ち防止)
lines = text.split('\n')
##################################
# 3.テキストデータを学習用データの形式に変換
##################################
#形態素解析器の定義
tagger = MeCab.Tagger("-d {}".format(unidic.DICDIR))
#除外する品詞、単語の定義
pos_filter = ['名詞', '動詞', '形状詞', '副詞', '形容詞'] #品詞pos(part_of_speech)
stop_words = [',', '"', "'",'…'] #ストップワード(除外ワード)
#テキストデータlinesを、学習用データdocumentsに変換
documents = [] #documentsはリストのリスト(sentenceを格納する)になる
for line in lines:
node = tagger.parseToNode(line) #ノードnode:形態素のこと
sentence = [] #リストsentenceには行毎の単語が格納される
while node:
token = node.surface #トークンtoken:要は単語のこと
pos = node.feature.split(',')[0]
if pos in pos_filter and token not in stop_words:
sentence.append(token)
node = node.next
documents.append(sentence)
###################################
# 4.モデルの制作と学習
##################################
model = word2vec.Word2Vec(documents, #documents=学習データ
vector_size=100, #単語のベクトル数(特徴量)=100に指定
window=5, #ある単語を予測する際に考慮する前後の単語数=5に指定
min_count=3, #学習する際、出現数が3回未満の単語を無視
epochs=5, #学習データ全体を5回繰り返して学習させる
seed=1) #ランダムシードの固定
##################################
# 5.回答の出力
##################################
A = input('日本語の「A - B + C」の計算をします。\n\nAを入力してください: ')
B = input('Bを入力してください: ')
C = input('Cを入力してください: ')
similars = model.wv.most_similar(positive=[A,C], negative=[B])
message = '\n(答え)\n{} - {} + {} = {}'.format(A,B,C,similars[0][0])
print(message)
##################################
#以下のコードは学習結果の確認用です
##################################
#message2 = "\n\n(参考)\n「答え」に近い単語のTOP5('単語',一致度(0〜1))\n"
#print(message2)
#for similar in similars[0:5]:
# print(similar)
1.5. 解説
要所の解説をします。
##################################
# 3.テキストデータを学習用データの形式に変換
##################################
#形態素解析器の定義
tagger = MeCab.Tagger("-d {}".format(unidic.DICDIR))
#除外する品詞、単語の定義
pos_filter = ['名詞', '動詞', '形状詞', '副詞', '形容詞'] #品詞pos(part_of_speech)
stop_words = [',', '"', "'",'…'] #ストップワード(除外ワード)
#テキストデータlinesを、学習用データdocumentsに変換
documents = [] #documentsはリストのリスト(sentenceを格納する)になる
for line in lines:
node = tagger.parseToNode(line) #ノードnode:形態素のこと
sentence = [] #リストsentenceには行毎の単語が格納される
while node:
token = node.surface #トークンtoken:要は単語のこと
pos = node.feature.split(',')[0]
if pos in pos_filter and token not in stop_words:
sentence.append(token)
node = node.next
documents.append(sentence)
tagger = MeCab.Tagger("-d {}".format(unidic.DICDIR))
MeCabを使用して形態素解析器を初期化しています。
-dオプションで辞書(ここではunidic)を指定しています。
documents = [] #documentsはリストのリスト(sentenceを格納する)になる
エラーが出やすいところでした。
Word2Vecモデルに使う学習データdocumentsは
[[単語のリスト]のリスト]であることが必要です。[単語のリスト]はNGです。
[[今日,は,晴れ,だ], [外,に,遊び,に,行こ,う]]というようなデータの入れ方が想定されています。
node = tagger.parseToNode(’形態素解析する文章’)
nodeはMeCabのNodeクラスのインスタンス(オブジェクト)となり、
形態素解析により分割された一つ一つの単語(形態素)の情報を保持します。
nodeオブジェクトは以下のような属性を持ちます:
surface: 形態素の表層形、つまりは実際のテキストデータが格納されています。
feature: 形態素の詳細情報が含まれます。ここには品詞、原形、読み方などが格納されています。node.feature.split(',')[0]で、品詞のみを表すことができます。
next、prev: これらは次または前のNodeオブジェクトへのリンクです。これにより形態素のリスト全体を繋がる連結リストとして扱うことができます。
###################################
# 4.モデルの制作と学習
##################################
model = word2vec.Word2Vec(documents, #documents=学習データ
vector_size=100, #単語のベクトル数(特徴量)=100に指定
window=5, #ある単語を予測する際に考慮する前後の単語数=5に指定
min_count=3, #学習する際、出現数が3回未満の単語を無視
epochs=5, #学習データ全体を5回繰り返して学習させる
seed=1) #ランダムシードの固定
model = word2vec.Word2Vec(学習データ,各ハイパーパラメーター)
Word2Vecモデルを制作し、学習させるコードです。
なお、seed=1で学習の初期乱数を固定してはいますが、
学習データの量が少ない時は特に予測結果が都度変わり、安定しません。
ChatGPT-4に質問してみたところ、以下のメカニズムによるそうです。
「Word2Vecの訓練プロセスはマルチスレッドで行われ、異なるスレッドがデータの異なる部分を同時に処理します。Pythonでは、マルチスレッドの動作はOSのスケジューラに依存し、スレッド間のタイミングは一定ではありません。これにより、訓練の順序が異なるため、完全な再現性は保証されません。」
##################################
# 5.回答の出力
##################################
A = input('日本語の「A - B + C」の計算をします。\n\nAを入力してください: ')
B = input('Bを入力してください: ')
C = input('Cを入力してください: ')
similars = model.wv.most_similar(positive=[A,C], negative=[B])
message = '\n(答え)\n{} - {} + {} = {}'.format(A,B,C,similars[0][0])
print(message)
similars = model.wv.most_similar(positive=['加算する単語'],
negative=['減算する単語'])
most_similar()関数は、( )内の単語に最も類似した単語とその類似度を返します。positive,negativeを使えば加算する語、減算する語をそれぞれ指定できます。
2. 実行結果
2.1. 失敗例
約11万語の学習データの場合です。
計算結果は「国土」になってしまいました。。
2位以下の候補は「ニューラル」「地区」「アルゴリズム」「成功」と続きます。
このように学習データが少ないと微妙な答えになりがちで、回答も一定しません。
学習範囲にない単語(あるいは学習範囲では出現数が少ない単語)を入力して、エラーを食らうこともしばしばです。
2.2. 成功例
約1100万語のデータで試した場合です。
期待通りの答えを返してくれました!
2位以下の候補も「都市名」のカテゴリーに属する単語でほぼ占められており、失敗例に比べて精度は格段に上がりました。
終わりに
機械学習を取り入れたプログラムを初めて作ってみましたが、有効に動くようになるためには、膨大なデータセットが必要であることがよくよく実感できました。
1100万語ものデータで学習を行った結果、ようやく期待に応える結果が得られ、データの収集と統合の難しさ、テキストデータを適切な形に整形する作業の大切さと手間を理解しました。
途中でつまづくことも多く、プログラミングを習っているスクールは大きな支えでした。情報収集が必要な時はブログ記事を頼りにし、これらの記事が非常に参考になりました。チューターの皆様、ブログ投稿者の皆様には感謝の気持ちでいっぱいです。
この経験を糧に、プログラミングや機械学習やAI、そしてそれらの可能性についての理解を深めていけたらなぁ〜と思います。