言語処理100本ノック 2020 (Rev2)の「第8章: ニューラルネット」の70本目「単語ベクトルの和による特徴量」記録です。
TensorFlowのTFRecordsが理解し難く、非常に時間がかかってしまいました。大規模データではデータ読込などでTFRecords使うメリットありそうですが、中規模レベルでpickleに比べて大きなメリットあるか、理解できていません。課題の数式は他の方のプログラムをカンニングしながらコーディングしているうちに理解できるので、最初に完全に理解しなくてもいいかと思います。
記事「まとめ: 言語処理100本ノックで学べることと成果」に言語処理100本ノック 2015についてはまとめていますが、追加で差分の言語処理100本ノック 2020 (Rev2)についても更新します。
参考リンク
リンク | 備考 |
---|---|
70.単語ベクトルの和による特徴量.ipynb | 回答プログラムのGitHubリンク |
言語処理100本ノック 2020 第8章: ニューラルネット | (PyTorchだけど)解き方の参考 |
【言語処理100本ノック 2020】第8章: ニューラルネット | (PyTorchだけど)解き方の参考 |
まとめ: 言語処理100本ノックで学べることと成果 | 言語処理100本ノックまとめ記事 |
TFRecords と tf.Example の使用法 | 今回の課題メイン |
言語処理100本ノック(2020)-50: データの入手・整形 | 前提ノック(元データ作成) |
言語処理100本ノック(2020)-60: 単語ベクトルの読み込みと表示 | 前提ノック(単語ベクトル読込) |
環境
後々GPUを使わないと厳しいので、Goolge Colaboratory使いました。Pythonやそのパッケージでより新しいバージョンありますが、新機能使っていないので、プリインストールされているものをそのまま使っています。
種類 | バージョン | 内容 |
---|---|---|
Python | 3.7.12 | Google Colaboratoryのバージョン |
2.0.3 | Google Driveのマウントに使用 | |
tensorflow | 2.6.0 | ディープラーニングの主要処理 |
pandas | 1.1.5 | タブ区切りファイル読込とその処理 |
gensim | 3.6.0 | Word2Vecのデータ読込処理 |
numpy | 1.19.5 | Numpy Arrayにする処理 |
第8章: ニューラルネット
学習内容
深層学習フレームワークの使い方を学び,ニューラルネットワークに基づくカテゴリ分類を実装します.
ノック内容
第6章で取り組んだニュース記事のカテゴリ分類を題材として,ニューラルネットワークでカテゴリ分類モデルを実装する.なお,この章ではPyTorch, TensorFlow, Chainerなどの機械学習プラットフォームを活用せよ.
70. 単語ベクトルの和による特徴量
問題50で構築した学習データ,検証データ,評価データを行列・ベクトルに変換したい.例えば,学習データについて,すべての事例$x_i$の特徴ベクトル$\boldsymbol{x}_i$を並べた行列$X$と正解ラベルを並べた行列(ベクトル)$Y$を作成したい.
X = \begin{pmatrix}
\boldsymbol{x}_1 \
\boldsymbol{x}_2 \
\dots \
\boldsymbol{x}_n \
\end{pmatrix} \in \mathbb{R}^{n \times d},
Y = \begin{pmatrix}
y_1 \
y_2 \
\dots \
y_n \
\end{pmatrix} \in \mathbb{N}^{n}
>
> ここで,$n$は学習データの事例数であり,$\boldsymbol x_i \in \mathbb{R}^d$と$y_i \in \mathbb N$はそれぞれ,$i \in \{1, \dots, n\}$番目の事例の特徴量ベクトルと正解ラベルを表す.なお,今回は「ビジネス」「科学技術」「エンターテイメント」「健康」の4カテゴリ分類である.$\mathbb N_{<4}$で$4$未満の自然数($0$を含む)を表すことにすれば,任意の事例の正解ラベル$y_i$は$y_i \in \mathbb N_{<4}$で表現できる.
> 以降では,ラベルの種類数を$L$で表す(今回の分類タスクでは$L=4$である).
>
> $i$番目の事例の特徴ベクトル$\boldsymbol x_i$は,次式で求める.
>
> $$\boldsymbol x_i = \frac{1}{T_i} \sum_{t=1}^{T_i} \mathrm{emb}(w_{i,t})$$
>
> ここで,$i$番目の事例は$T_i$個の(記事見出しの)単語列$(w_{i,1}, w_{i,2}, \dots, w_{i,T_i})$から構成され,$\mathrm{emb}(w) \in \mathbb{R}^d$は単語$w$に対応する単語ベクトル(次元数は$d$)である.すなわち,$i$番目の事例の記事見出しを,その見出しに含まれる単語のベクトルの平均で表現したものが$\boldsymbol x_i$である.今回は単語ベクトルとして,問題60でダウンロードしたものを用いればよい.$300$次元の単語ベクトルを用いたので,$d=300$である.
> $i$番目の事例のラベル$y_i$は,次のように定義する.
>
>```math
y_i = \begin{cases}
0 & (\mbox{記事}\boldsymbol x_i\mbox{が「ビジネス」カテゴリの場合}) \\
1 & (\mbox{記事}\boldsymbol x_i\mbox{が「科学技術」カテゴリの場合}) \\
2 & (\mbox{記事}\boldsymbol x_i\mbox{が「エンターテイメント」カテゴリの場合}) \\
3 & (\mbox{記事}\boldsymbol x_i\mbox{が「健康」カテゴリの場合}) \\
\end{cases}
なお,カテゴリ名とラベルの番号が一対一で対応付いていれば,上式の通りの対応付けでなくてもよい.
以上の仕様に基づき,以下の行列・ベクトルを作成し,ファイルに保存せよ.
- 学習データの特徴量行列: $X_{\rm train} \in \mathbb{R}^{N_t \times d}$
- 学習データのラベルベクトル: $Y_{\rm train} \in \mathbb{N}^{N_t}$
- 検証データの特徴量行列: $X_{\rm valid} \in \mathbb{R}^{N_v \times d}$
- 検証データのラベルベクトル: $Y_{\rm valid} \in \mathbb{N}^{N_v}$
- 評価データの特徴量行列: $X_{\rm test} \in \mathbb{R}^{N_e \times d}$
- 評価データのラベルベクトル: $Y_{\rm test} \in \mathbb{N}^{N_e}$
なお,$N_t, N_v, N_e$はそれぞれ,学習データの事例数,検証データの事例数,評価データの事例数である.
回答
回答結果
find
コマンドでGoogle Driveへ出力したファイルを確認。ノック内容に少し反していますが、特徴量行列とラベルベクトルは同じファイルにまとめています。
77 1640 -rw------- 1 root root 1679352 Sep 23 01:09 /content/drive/MyDrive/ColabNotebooks/ML/NLP100_2020/08.NeuralNetworks/valid.tfrecord
82 1640 -rw------- 1 root root 1679352 Sep 23 01:09 /content/drive/MyDrive/ColabNotebooks/ML/NLP100_2020/08.NeuralNetworks/test.tfrecord
72 13116 -rw------- 1 root root 13429788 Sep 23 01:09 /content/drive/MyDrive/ColabNotebooks/ML/NLP100_2020/08.NeuralNetworks/train.tfrecord
回答プログラム 70_単語ベクトルの和による特徴量.ipynb
GitHubには確認用コードも含めていますが、ここには必要なものだけ載せています。
import string
import tensorflow as tf
import pandas as pd
import numpy as np
from google.colab import drive
from gensim.models import KeyedVectors
drive.mount('/content/drive')
BASE_PATH = '/content/drive/MyDrive/ColabNotebooks/ML/NLP100_2020/'
BASE_PATH08 = BASE_PATH + '08.NeuralNetworks/'
w2v_model = KeyedVectors.load_word2vec_format(BASE_PATH+'07.WordVector/input/GoogleNews-vectors-negative300.bin.gz', binary=True)
# 変換テーブル作成: 同じ長さでないとエラーがでるので第2引数の長さ調整(置換元と置換先文字は1:1だから)
table = str.maketrans(string.punctuation, ' '*len(string.punctuation))
def vectorize(title):
# 句読点をスペースに置換してsplit
tokens = title.translate(table).split()
# 各Tokenをベクトル化
vectors = [w2v_model[token] for token in tokens if token in w2v_model]
# Tokenごとの平均ベクトルを算出(DataFrameの要素にNumpy Arrayを格納)
vectors = np.array(sum(vectors)/len(vectors))
return vectors
def make_example(title, category):
# Dictionary形式でfeaturesを格納
features={
'title' : tf.train.Feature(bytes_list=tf.train.BytesList(value=[title])),
'category' : tf.train.Feature(bytes_list=tf.train.BytesList(value=[category]))
}
# ExampleとFeaturesでまとめて返す
return tf.train.Example(features=tf.train.Features(feature=features))
def process_data(type_):
df = pd.read_table(BASE_PATH08+'input/'+type_+'.txt')
# タイトル: Token化しベクトル化した要素を1列にしたSeries作成
titles = df['title'].map(vectorize).explode().astype('float32')
# タイトル: 配列をNumpy形式にしてshape変更
titles = titles.values.reshape(len(df), 300)
# ラベル: 文字を数字に変更
categories = df['category'].replace({'b':0, 't':1, 'e':2, 'm':3})
# 本当はScikit Learnの関数が望ましいが少ないラベル(4値)種類でそこそこデータ件数があるのでkeras使う
categories = tf.keras.utils.to_categorical(categories, dtype='int32')
# ファイル書込
with tf.io.TFRecordWriter(BASE_PATH08+type_+'.tfrecord') as writer:
# 1行ずつ取り出してExample形式にして書込
for title, category in zip(titles, categories):
ex = make_example(title.tobytes(), category.tobytes())
writer.write(ex.SerializeToString())
process_data('train')
process_data('valid')
process_data('test')
回答解説
ノック解説
まずはノック内容自体の解説からします。
すべての事例$x_i$の特徴ベクトル$\boldsymbol{x}_i$を並べた行列$X$と,正解ラベルを並べた行列(ベクトル)$Y$を作成したい.
上記部分は以下のように整理できます。
項目 | 内容 | 例 |
---|---|---|
事例$x_i$ | i番目の事例 | Jj Abrams - Star Wars: Episode VII has started filming |
特徴ベクトル$\boldsymbol{x}_i$ | i番目の特徴ベクトル(300要素の特徴ベクトル) | [ 0.117, ..., 0.024] 300要素 |
行列$X$ | 特徴ベクトル$\boldsymbol{x}_i$が事例数並んだ行列 | [[ 0.117, ..., 0.024],...[0.117, ..., 0.024]]300要素×事例数 |
行列$Y$ | 正解ラベル$y_i$が事例数並んだ行列 | [0, 1, ..., 2]事例数と同じ要素 |
$\mathbb{R}^{n \times d}や\mathbb{N}^{n}$の$n$は事例数で$d$は次元数(今回はノック60で取得したWord2Vecモデルの次元数300)。
X = \begin{pmatrix}
\boldsymbol{x}_1 \\
\boldsymbol{x}_2 \\
\dots \\
\boldsymbol{x}_n \\
\end{pmatrix} \in \mathbb{R}^{n \times d},
Y = \begin{pmatrix}
y_1 \\
y_2 \\
\dots \\
y_n \\
\end{pmatrix} \in \mathbb{N}^{n}
特徴ベクトル$\boldsymbol{x}_i$ の計算についてです。
\boldsymbol{x}_i = \frac{1}{T_i} \sum_{t=1}^{T_i} \mathrm{emb}(w_{i,t})
仮に10番目の事例に「Chariklo asteroid has two RINGS」があったとします。
項目 | 内容 | 例 |
---|---|---|
事例$x_{10}$ | 10番目の事例 | Chariklo asteroid has two RINGS |
単語列数$T_{10}$ | 10番目の事例の単語列数 | 5(事例の文は5単語) |
単語列$w_{10,t}$ | 10番目の事例の単語列 | (Chariklo, asteroid, $\dots$, RINGS) |
単語ベクトル$\mathrm{emb}(w_{10,t}$ | 10番目の事例の単語ベクトル | Word2Vecで取得する300次元の単語ベクトル |
事例のベクトル化
関数vectorize
で事例をベクトル化しています。pandas
のSeries
の1要素に対してmap
関数で実行されます。つまり、入力パラメータのtitle
はstring型のテキスト(例: 「Jj Abrams - Star Wars: Episode VII has started filming」)です。
len(vectors)
は単語数$T_i$です。300次元の各要素を全単語で平均化しているため、常に出力vectors
が300次元になります。私は途中まで、各単語ごとに平均ベクトルを求めるのかと勘違いをしていました。
# 変換テーブル作成: 同じ長さでないとエラーがでるので第2引数の長さ調整(置換元と置換先文字は1:1だから)
table = str.maketrans(string.punctuation, ' '*len(string.punctuation))
def vectorize(title):
# 句読点をスペースに置換してsplit
tokens = title.translate(table).split()
# 各Tokenをベクトル化
vectors = [w2v_model[token] for token in tokens if token in w2v_model]
# Tokenごとの平均ベクトルを算出(DataFrameの要素にNumpy Arrayを格納)
vectors = np.array(sum(vectors)/len(vectors))
return vectors
TFRecord形式での保存
今回、理解するのに最も時間がかかった内容です。主にチュートリアル「TFRecords と tf.Example の使用法」を見て理解しましたが、以下の点が重要。データ読込が遅くなければ使わなくてもいい。
tf.data を使っていて、それでもデータの読み込みが訓練のボトルネックであるという場合でなければ、既存のコードを TFRecords を使用するために変更する必要はありません。
以下の順序でTFRecord形式で保存します。
-
tf.train.Feature
形式に変換 - Dictionary型データにして
tf.train.Features
メッセージに変換 -
tf.train.Example
メッセージに変換 - シリアライズしてTFRecord形式でファイル書込
事例およびラベルデータをtf.train.Feature
形式に変換します。tf.train.Feature
は以下の3種類の型を選びます。詳しくは「tf.Example 用のデータ型」を参照。
-
tf.train.BytesList
: Stringや複数のNumpy配列もこれ。 tf.train.FloatList
tf.train.Int64List
今回は、1行ずつ配列から取り出して、title
およびcategory
は複数列なので、Byte化しています。
# ファイル書込
with tf.io.TFRecordWriter(BASE_PATH08+type_+'.tfrecord') as writer:
# 1行ずつ取り出してExample形式にして書込
for title, category in zip(titles, categories):
ex = make_example(title.tobytes(), category.tobytes())
writer.write(ex.SerializeToString())
で、関数make_example
内でtf.train.Feature
に渡しています。
def make_example(title, category):
# Dictionary形式でfeaturesを格納
features={
'title' : tf.train.Feature(bytes_list=tf.train.BytesList(value=[title])),
'category' : tf.train.Feature(bytes_list=tf.train.BytesList(value=[category]))
}
# ExampleとFeaturesでまとめて返す
return tf.train.Example(features=tf.train.Features(feature=features))