概要
個人的な備忘録を兼ねたPyTorchの基本的な解説とまとめです。
MLM事前学習編(第29回〜第32回)では、TensorDatasetとDataLoaderを組み合わせて学習データを扱ってきました。TensorDatasetを使う都合上、学習データの系列長をあらかじめ揃えておく必要がありました。系列長はサンプルごとにバラバラなのが普通です。系列長の異なるデータでも自然に扱える方法を考えてみたいと思います。
そのためには、イメージ図のようにDatasetクラスを継承してカスタマイズする必要がありそうです。というわけで今回からしばらくDatasetクラスやDataLoaderクラスのカスタマイズについてまとめておきたいと思います。
公式のチュートリアル
扱いたいテーマ
- Datasetクラスのカスタマイズ方法 (今回・第33回)
- DatasetとDataLoaderのコンボ・collate_fnを使ってみる (第34回)
- 文章分類で試してみる
- 可変長データでのBERTタイプの事前学習
- HuggingFaceのライブラリーとつなげてみる (予定)
演習用のファイル
- サンプルデータ1:ramen_data.csv
- ラーメンの種類分類の架空のデータセット
- サンプルデータ2:history_text_label_id.jsonl
- wikipediaの日本史関連から取得した時代区分分類のテキストデータ
- サンプルデータ3:greek_wiki.txt
- wikipediaのギリシア神話のテキストデータ
- トークナイザー: unigram_tokenizer_2k.json
- wikipediaの日本に関連を利用して作成したトークナイザー
- トークナイザー:greek_unigram_tokenizer_3k.json
- wikipediaのギリシア神話を利用して作成したトークナイザー
- コード: sample_33.ipynb
- 登場するコードをひとまとめにしたもの
1. Datasetクラスの基本編
- Datasetクラスを継承して自作のDatasetクラス作成するの難しそう
- そもそも何をすればいいの?
ということで公式のDocumentを参照してみました。
ドキュメントによると、__getitem__()と__len__()を上書きせよと明記されています1。
実際はインスタンス化して利用するので、カスタム版Datasetクラスを作成するには、
-
__init__(): 初期化、入力されるデータによって変更 -
__len__(): データの長さ取り出し -
__getitem__(): データ取り出し
の特殊メソッド3種類を定義することになります。入力されたdataに対してlen(data)でサイズを、data[ ]で添字アクセスできるようにするのがこのクラスの役割となります2。
1.1 基本的な使い方
初期化時の引数となるdataは、リスト、numpy配列やpandasのデータフレーム、torch.tensor、文章データなど様々です。基本形から確認してみたいと思います。
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self, data):
self.data = data
def __len__(self):
return len(self.data)
def __getitem__(self, index):
return self.data[index]
説明メモ
- self.data = data: 次の2つを定義するのに使う
- __len__():
len(data)でdataの長さを取得できるようにする - __getitem__():
data[0]で変数dataの0番目の値を取得できるようにする
dataがリストの場合で確認してみたいともいます。リストだとlen(data), data[ ]が既にあるのですが、触れないでおいてください😅
data = [3, 2, 1, 0, 1, 2, 3]
dataset = MyDataset(data)
# (1) __len__の使い方
print(dataset.__len__()) # 7
print(len(dataset)) # 7 通常の書き方
# (2) __getitem__の使い方
print(dataset.__getitem__(1)) # 2
print(dataset[1]) # 2 通常の書き方
説明メモ
__len__()や__getitem__()は、通常のクラスメソッドとは異なる方法で利用することができます。
- (1) len(dataset) で、
dataset.__len__()と同じ働きになります。datasetの長さを取得するという意味です3。 - (2) dataset[1] で、
dataset.__getitem__(1)と同じ働きになります。datasetの1番目の値を取得するという意味です。
1.2 Numpy配列用に修正したDatasetクラス
ラーメン分類問題
(第3回)で利用したCSVファイルを使ってみたいと思います。利用するデータはramen_data.csvです。
表1:ramen_data.csvのサンプル
| 脂質含有量 | スープの濃度 | 塩分濃度 | 後味の持続時間 | 透明度 | ラーメンタイプ | ラーメンタイプID |
|---|---|---|---|---|---|---|
| 3.2 | 3.9 | 5.5 | 1.9 | 8.7 | あっさり | 0 |
| 2.8 | 3.7 | 5.2 | 1.5 | 5.9 | あっさり | 0 |
| 3.7 | 3.9 | 5.1 | 1.8 | 5.8 | あっさり | 0 |
| 2.7 | 5.1 | 5.0 | 2.2 | 7.2 | あっさり | 0 |
| 3.3 | 4.1 | 4.9 | 1.6 | 6.5 | あっさり | 0 |
表1の「脂質含有量〜透明度」列までを入力されるデータxとします。最後列のラーメンタイプID列を分類ラベルデータtとします。
import numpy as np
x = np.loadtxt("data/ramen_data.csv", delimiter=",", skiprows=1, usecols=(0,1,2,3,4))
t = np.loadtxt("data/ramen_data.csv", delimiter=",", skiprows=1, usecols=(6))
この(x,t)に対応したDatasetクラスにカスタマイズしてみます。torch.tensor化することで素直にTensorDatasetが利用できるのですが、再び、あえて触れないようにしましょう😆
class MyDataset02(Dataset):
# (1) 引数が2種類(x, t)
def __init__(self, x,t):
self.x = x # 入力データ
self.t = t # 教師データ
# (2) どちらかのサイズでOK
def __len__(self):
return len(self.t)
# (3) 入出力するデータのタイプによって適宜修正
def __getitem__(self, index):
return {
"data": np.array(self.x[index], dtype=np.float32),
"label": np.array(self.t[index], dtype=np.int64)
}
from torch.utils.data import Datasetは共通なので省略しました ![]()
説明メモ
- (1) 引数がnumpy配列の 入力データ (x) と教師データ (t) の2種類なので、(x, t)のペアで準備しておきます。
- (2) データ数ですが、tの長さで代用しました。他にも方法があると思います。
- (3) なんとなく格好いいかと思い、returnに辞書を使ってみました🐾
- 🌴__getitem__の戻り値、numpy配列にしてみました
dataset = MyDataset02(x,t)
print(len(x)) # 500
print(len(t)) # 500
print(dataset[0]) # {'data': array([3.2, 3.9, 5.5, 1.9, 8.7], dtype=float32), 'label': array(0)}
print(dataset[0]["data"]) # [3.2 3.9 5.5 1.9 8.7]
print(dataset[0]["label"]) # 0
# 参考
# from torch.utils.data import random_split
# train, test = random_split(dataset, [0.8, 0.2])
- dataset[0]の値が辞書になっていることが確認できます。なんとなく格好いい😆
- datasetの出力値がnumpy配列の辞書なのであとでtorch.tensor化する必要があります。
- torch.utils.data.random_split を利用するとDatasetオブジェクトをランダムに分割することができます。train_test_splitを使わなくてもOKということになります。
1.3 データフレーム用に修正したDatasetクラス
JSONL形式で保存した歴史時代区分テキスト分類用のデータセット
(第32回)を使ってみます。初期化時のdataがデータフレームになるパターンです。データの読み込みにpandasを利用しています。
import pandas as pd
data_filename = "./data/history_text_label_id.jsonl" # 分類問題のデータ
df = pd.read_json(data_filename, lines=True)
表2:dfのサンプル
| text | label | ids | |
|---|---|---|---|
| 197 | 児童福祉法を公布させた。 | 0 | [1, 1006, 3, 1976, 3, 365, 7, 402, 711, 240, 6...] |
| 597 | 彼らは城下に住み藩主から俸禄をもらって... | 2 | [1, 1951, 49, 11, 370, 79, 9, 604, 114, 3, 303...] |
| 249 | なお、経路はこのうち一つかもしれないし、... | 1 | [1, 31, 1932, 8, 571, 1494, 11, 51, 121, 184, ...] |
学習時に利用したいのは、教師ラベルとなるlabel列と系列長が異なるids列になります。
Datasetを継承してカスタマイズすることになるのですが、自由度が大きすぎ!利用目的に合わせて範囲を制限します。
MyDataset03で行いたいこと
- dataset = MyDataset03(df)という形で使うとしましょう
- dataset[0]はidsとlabelキーを持つ辞書形式
- 出力されるデータはtorch.tensorでtorch.long型
import torch # return部分のtorch.tensorで使う
class MyDataset03(Dataset):
# (1) dataがデータフレームになっている
def __init__(self, data):
self.data = data
# (2) 他の方法でもOK
def __len__(self):
return len(self.data)
# (3) データの形式で適宜修正
def __getitem__(self, index):
item = self.data.iloc[index] # データフレームのindex行を取得したいので data.iloc[]を使う
tensor_x = torch.tensor(item["ids"], dtype=torch.long)
tensor_t = torch.tensor(item["label"], dtype=torch.long)
return {"ids": tensor_x, "label": tensor_t}
# 次の形だとdfの値なのでリストや数値でreturnとなります
#return {"ids": item["ids"], "label": item["label"]}
説明メモ
-
(3) __getitem__()の部分を変更します。データフレームの各行のidsとlabelの値を取得したいので、iloc[index] を使います。これで、index行の列データにアクセスできるはず
-
item = self.data.iloc[index]行のidsとlabelのデータを取得したいので、item["ids]とitem["label"]を使います。
-
Datasetクラスの中でtorch.tensor化してみました。
tensor_x = torch.tensor(item["ids"], dtype=torch.long)
tensor_t = torch.tensor(item["label"], dtype=torch.long)としてみました。
-
戻り値、タプルでも良いのですが、なんとなく使い勝手が良さそうなので、辞書形式でまとめてみました。
return {"ids": ..., "label": ...}
となります。
torch.tensorの値を持つ辞書形式になっていることを出力で確認してみます。
dataset = MyDataset03(df)
print(len(dataset)) # 1000
print(dataset[1])
# {'ids': tensor([ 1, 29, 227, 299, 671, 68, 57, 258, 1594, 1892, 8, 68, 469, 131, 1919, 1892, 5, 55, 1442, 27, 6, 2]),
# 'label': tensor(0)}
1.4 データフレーム版・__init__でリストにするパターン
データフレームの行データにアクセスするilocを使わず、__init__部分でリストにしてみました。なんとなく汎用性がある書き方っぽい?
MyDataset04で行いたいこと
- dataset = MyDataset04(data)という形で使う
- __init__部分でリスト化
- 出力されるデータはリスト(torch.tensor化は後で実行)
class MyDataset04(Dataset):
# (1) データフレームをtolist()でリストへ
def __init__(self, data):
self.ids = data["ids"].tolist()
self.label = data["label"].tolist()
def __len__(self):
return len(self.label)
# (2) (1)でリストになっているので添字アクセス可能
def __getitem__(self, index):
return {"ids": self.ids[index],
"label": self.label[index]}
説明メモ
- (1) データフレームをリストへ変換
- (2) リストデータに対して添字アクセス。今回はリストのままで戻り値に指定してみました。numpy配列やtensorでもOKです。
dataset = MyDataset04(df)
print(len(dataset)) # 1000
print(dataset[1])
# {'ids': [1, 29, 227, 299, 671, 68, 57, 258, 1594, 1892, 8, 68, 469, 131, 1919, 1892, 5, 55, 1442, 27, 6, 2],
# 'label': 0}
まとめメモ
Datasetクラスを継承してカスタムDatasetクラスを作成するには、初期化時の引数dataに対して
- __init__() : 初期化、入力されるデータによって適宜に変更
- __len__() : データの長さ取り出し
- __getitem__() : データ取り出し
の特殊メソッド3種類を定義する。
ここまでが基本編となります。クラスの設計なのでかなり自由度があります。方針を決めないと、おそらくコードが読みにくくなる温床になりかねない😱
😱
2. 応用編
Datasetクラスは特殊メソッド3種類を定義すればよいので、系列長を同じ長さに変える処理やテキストデータをID化する処理を含めることもできそうです。「画像+テキストデータ」「音声データ+テキストデータ+ラベル」みたいなマルチモーダル処理も余裕綽々![]()
2.1 前処理も入れてしまうパターン (__init__部分)
テキスト分類問題のids列はデータごとに系列長が異なっていました。__init__部分で系列長を等長化する方法を行ってみます。事前に等長化するほうが楽そう〜ということは触れないでおいてください😅
pytorchでpaddingといえば pad_sequence。pad_sequenceはリスト内で一番長い系列のサイズに合わせてpaddingしてくれるお役立ちメソッドです。pad_sequenceを利用すると等長化の作業は簡単にできるはず。
MyDataset05で行いたいこと
- dataset = MyDataset05(df)という形で使いたい
- df["ids"]列をtorch.tensor化
- pad_sequenceを利用して等長化処理も含めてみた
- 等長化ベクトルとラベルを出力させる
import torch
from torch.nn.utils.rnn import pad_sequence
class MyDataset05(Dataset):
# pad_sequenceを追加して、すべてのidsが等長化させるぞ〜
def __init__(self, data):
# (1) 各idsをtorch.tensorに変換、リストとします。
ids_list = [torch.tensor(x, dtype=torch.long) for x in data["ids"]]
labels = torch.tensor(data["label"].tolist(), dtype=torch.long)
# (2) dataのids列をすべて等長化
ids_padded = pad_sequence(ids_list, batch_first=True, padding_value=0)
self.ids_padded = ids_padded
self.labels = labels
def __len__(self):
return len(self.labels)
# (3) ids_paddedやlabelsはテンソルになっています
def __getitem__(self, index):
return {"ids": self.ids_padded[index],
"label": self.labels[index]}
説明メモ
- (1) データフレームの"ids"列を対象として、それぞれの行をtorch.tensor化します。
- label列も各行ごと、torch.tensorにします。
- (2) pad_sequenceを使って、"ids"列の最大系列長のすべての行をpadding🐾
- (3) __getitem__は、素朴にテンソルを戻り値に指定するだけです。
イニシャライズ時に一気に処理する形4だから、データのサイズが大きいとNGっぽい予感もします。実際どうなんだろう🤔
# df はテキスト分類のデータフレーム 表2
dataset = MyDataset05(df)
dataloder = DataLoader(dataset, batch_size=2,shuffle=True)
print(next(iter(dataloder)))
# {'ids': tensor([[ 1, 170, 1119, 1110, 257, 470, 11, 238, 187, 650, 26, 17,
# 662, 88, 1017, 1589, 534, 116, 5, 1943, 1062, 469, 187, 258,
# 258, 817, 7, 324, 997, 18, 1943, 257, 252, 3, 1366, 261,
# 5, 1943, 10, 1497, 440, 7, 62, 837, 102, 24, 21, 6,
# 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0],
# [ 1, 248, 8, 672, 1278, 134, 200, 5, 560, 614, 7, 539,
# 635, 22, 349, 11, 273, 313, 1167, 30, 1963, 73, 7, 1075,
# 41, 17, 608, 420, 81, 5, 111, 24, 20, 6, 2, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0]]),
# 'label': tensor([3, 4])}
表示していませんが、idsの長さがすべて64になっています。
2.2 前処理も入れてしまうパターン2(__getitem__部分)
__getitem__部分でも等長化できないか?と試してみました。いやぁ〜、逆に面倒になりました![]()
__getitem__は、番号でアクセスするメソッドなので、一括して処理みたいなのが無理そう。苦肉の策ですが、最大長を事前に指定して、その長さになるように、index行ごとに対応する形にしました😔
ポジティブに考えて、行ごとに処理するときは__getitem__が使えそうです。
class MyDataset06(Dataset):
# (1) 準備
def __init__(self, data, max_length=64):
self.ids = data["ids"].tolist() # paddingしやすいようにリスト化
self.label = data["label"].tolist()
self.max_length = max_length # 最大系列長を指定
def __len__(self):
return len(self.label)
def __getitem__(self, index):
# (2) index行をmax_lengthまでpadding
ids = self.ids[index] # index番目のindex行だけ取り出す
ids = ids[:self.max_length] # 長すぎたら切り詰める
ids = ids + [0] * (self.max_length - len(ids)) # 短い分を0で埋める
return {
"ids": torch.tensor(ids, dtype=torch.long),
"label": torch.tensor(self.label[index], dtype=torch.long),
}
説明メモ
- (1) max_lengthで系列の最大長をしていします。今回は、
max_length=64です。 - (2) index行をmax_lengthの長さまで「0」で埋めます。
+[0]*必要な長さで対応。
dataset = MyDataset06(df)
dataloader = DataLoader(dataset, batch_size=2)
next(iter(dataloader))
# {'ids': tensor([[ 1, 29, 90, 5, 46, 8, 88, 97, 5, 105, 6, 2,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0],
# [ 1, 29, 227, 299, 671, 68, 57, 258, 1594, 1892, 8, 68,
# 469, 131, 1919, 1892, 5, 55, 1442, 27, 6, 2, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 0, 0]]),
# 'label': tensor([0, 0])}
- paddingの例だと、あまり魅力を感じませんが、きっと他の時、役立つパターンなんだろうと思いたい。
2.3 tokenizerを__init__に入れてみた
tokenizerを使ったid化を組み込んでみました。文章データとtokenizerをDatasetクラスの引数に使えば、文章のトークナイズもDatasetで対応できそうです。
- 準備するデータが文書だけでOK
- tokenizerを変更しても、それに応じてid列が生成される (これがとても有効な気がします😆)
テキスト分類のデータのtext部分を使って実験してみたいと思います。
MyDataset07で行いたいこと
- dataset = MyDataset07(df, tokenizer)という形で使いたい
- df["ids"]列のように、df["text"]列をID化する際に文頭に<bos>、文末に<eos>のIDを挿入する
- paddingして等長化も行いたい
- ラベルも出力させる
import torch
from torch.nn.utils.rnn import pad_sequence
# tokenizerで使う
from tokenizers import Tokenizer
from tokenizers.processors import TemplateProcessing
# (1) idsで利用したtokenizerと同じものを指定
tokenizer_filename = "tokenizer/unigram_tokenizer_2k.json"
tokenizer = Tokenizer.from_file(tokenizer_filename)
bos_id = tokenizer.token_to_id("<bos>")
eos_id = tokenizer.token_to_id("<eos>")
# (2) encodeするときのテンプレ
tokenizer.post_processor = TemplateProcessing(
single="<bos> $A <eos>",
special_tokens=[("<bos>", bos_id), ("<eos>", eos_id)],
)
class Dataset07(Dataset):
def __init__(self, data, tokenizer):
# (3) encode_batchで一度にid化
# self.tokenizer = tokenizer Dataset08では有効化
encodings = tokenizer.encode_batch(list(data["text"]))
ids_list = [torch.tensor(e.ids, dtype=torch.long) for e in encodings]
# (4) 確認用 ids列の値と比較する
org_ids_list = [torch.tensor(x, dtype=torch.long) for x in data["ids"]]
# (5) ラベル
labels = torch.tensor(data["label"].tolist(), dtype=torch.long)
# (6) 一応、等長化してみた
pad_id = tokenizer.token_to_id("<pad>")
ids_padded = pad_sequence(ids_list, batch_first=True, padding_value=pad_id)
self.ids_padded = ids_padded
self.labels = labels
self.org_ids_list = org_ids_list
def __len__(self):
return len(self.labels)
def __getitem__(self, index):
return {"ids": self.ids_padded[index],
"org_ids": self.org_ids_list[index],
"label": self.labels[index]
}
説明メモ
- (1) tokenizerの指定。ここを変更するとtokenizerに応じたID列となるので実験時に有用なはず。
- (2) TemplateProcessingでencodeする時のテンプレートを指定できる5。
$Aが文章で、<bos>や<eos>などの特殊トークンを配置するだけです。書き方もテンプレ😆 - (3) encode_batchでdata["text"]を一括してエンコード!pad_sequenceを使うために、tensorのリストにしておきます。
- (4) 実験用で本来不要なのですが、ids列の値を見るための準備です🐾
- (5) label列の取得
- (6) pad_sequenceを利用して、等長化します。tokenizerを使っているので、いままで「0」と直接記入していた部分に
tokenizer.token_to_id("<pad>")が使えます。
結果を確認してみましょう♪ id列とtext列をid化した値が等しくなっています![]()
dataset = Dataset07(df, tokenizer)
dataset[0]
# {'ids': tensor([ 1, 29, 90, 5, 46, 8, 88, 97, 5, 105, 6, 2, 0, 0, 0,...0, 0]),
# 'org_ids': tensor([ 1, 29, 90, 5, 46, 8, 88, 97, 5, 105, 6, 2]),
# 'label': tensor(0)}
<pad>の挿入が無駄に多い!一括で全部を等長化するのは効率がよくなさそう 😅 これは次回のエクササイズ。
__init__で一括してtokenizerによるID化を行っているので、テキストデータが大規模化するとNGっぽい香りがする。__getitem__でtokenizerを使うことで、indexごとにid化できそうだ🌵
2.4 tokenizerを__getitem__に入れてみた
MyDataset08で行いたいこと
- dataset = MyDataset08(df, tokenizer)という形で使いたい
- テキスト
df["text"]をID化する際、文頭に<bos>、文末に<eos>のIDを挿入する - __getitem__を利用して、indexごとにid列を作成
- ラベルも出力させる
文頭、文末の処理は TemplateProcessingを使うのでDataset07の例と同じになります。大きく異なるのは、__init__と__getitem__部分です。__init__ではリストに変換するだけです。トークナイズは__getitem__で行います。
class Dataset08(Dataset):
def __init__(self, data, tokenizer):
self.tokenizer = tokenizer # getitemで使う
# (1) text列をそのまま保持
self.text_list = data["text"].tolist()
self.org_ids_list = [torch.tensor(x, dtype=torch.long) for x in data["ids"]]
self.labels = torch.tensor(data["label"].tolist(), dtype=torch.long)
def __len__(self):
return len(self.labels)
def __getitem__(self, index):
# (2) 取り出すタイミングで1件だけencodeする
ids = torch.tensor(self.tokenizer.encode(self.text_list[index]).ids, dtype=torch.long)
return {"ids": ids,
"org_ids": self.org_ids_list[index],
"label": self.labels[index]}
説明メモ
- (1) データフレームのtext列をリスト (
text_list) に変換します。 - (2)
text_list[index]をエンコードします。
dataset = Dataset08(df, tokenizer)
dataset[0]
# {'ids': tensor([ 1, 29, 90, 5, 46, 8, 88, 97, 5, 105, 6, 2]),
# 'org_ids': tensor([ 1, 29, 90, 5, 46, 8, 88, 97, 5, 105, 6, 2]),
# 'label': tensor(0)}
<pad>で等長化していないので、idsとorg_idsが等しくなります。このタイプだとDataLoaderのcollate_fnを活用してpaddingすることになります。
2.5 テキストデータ(Transformer Decoderタイプ)
2.3と2.4では文章データと分類ラベルでDatasetクラスの出力を構成しました。ラベルデータ部分を1個ずれトークンにするTransformer Decoderタイプに対応するDatasetクラスも作成してみたいと思います。
利用するのはwikipediaのギリシア神話から抽出した文章をテキストデータとunigram LMで学習したトークナイザーとなります。
MyDataset09で行いたいこと
- dataset = MyDataset09(text, tokenizer)という形で使いたい
- 入力した文 (text) を tokenizer でID化
- context_sizeを系列長としたidsを作成する。context_size長のID列を入力データ (ids) とする。context_sizeが窓になる形。
- 1トークンだけずれたcontext_sizeの系列長idsを作成し、これを次のトークン予測で利用する教師データ (labels) とする。
- 入力データはstride数トークンずらした形で作成する。
import torch
from tokenizers import Tokenizer
# 今回利用するトークナイザー
tokenizer_filename = "tokenizer/greek_unigram_tokenizer_3k.json"
tokenizer = Tokenizer.from_file(tokenizer_filename)
class MyDataset09(Dataset):
# (1) tokenizer.encode(text).idsがtextのID列
def __init__(self, text, tokenizer, context_size=16, stride=2):
self.context_size = context_size
self.ids = torch.tensor(tokenizer.encode(text).ids, dtype=torch.long)
# (2) 各サンプルの開始位置をあらかじめ計算
self.starts = list(range(0, len(self.ids) - context_size, stride))
# データ数
def __len__(self):
return len(self.starts)
# (3)
# startからcontext_sizeまでが入力データ
# start+1からcontext_sizeまでが次のトークン予測のラベルデータ
def __getitem__(self, idx):
# 入力とターゲットのペアを作成
start = self.starts[idx]
x = self.ids[start:start + self.context_size]
y = self.ids[start + 1:start + self.context_size + 1]
return {"ids": x, "labels": y}
説明メモ
- (1) textは文章データです。1文1行などの修正も不要です。textをまるごとencodeします。これで、文章がID列 (ids) になります。
- strideは、context_sizeの窓の移動量
- (2) rangeの機能を使い開始位置をリストにします。stride=1なら1個ずれで、stride=context_sizeなら重なりがない分割の形となります。
- (3) 「context_size」を窓としてidsに適用します。先頭 (start) からcontext_size個のトークンを入力データ、次の開始位置 (start+1) からcontext_size個を教師データとします。stride数のトークンがずれる形になります。
with open("./data/greek_wiki.txt","rt") as file:
text = file.read()
dataset = MyDataset09(text, tokenizer, context_size=16, stride=2)
dataset[0]
# {'ids': tensor([ 131, 79, 382, 8, 144, 824, 22, 357, 2190, 172, 446, 858,2289, 7, 2216, 15]),
# 'labels': tensor([ 79, 382, 8, 144, 824, 22, 357, 2190, 172, 446, 858, 2289,7, 2216, 15, 147])}
dataset[1]
# {'ids': tensor([ 382, 8, 144, 824, 22, 357, 2190, 172, 446, 858, 2289, 7, 2216, 15, 147, 103]),
# 'labels': tensor([ 8, 144, 824, 22, 357, 2190, 172, 446, 858, 2289, 7, 2216, 15, 147, 103, 56])}
ファイルの内容を一括してtextとしています。dataset[0]のidsとlabelsが1トークンずれていることがわかります。dataset[1]のidsは、dataset[0]の2トークン (stride=2) ずれidsからスタートしています。
tokenizerを変更すれば、それに応じてid列が生成されるので実験的な場面での活躍しそう。しかし!これメモリー足りなくなるタイプ
file.read()で一挙に文章読み込んでるし、init()の部分でtokenizer.encode(text)と一括してエンコードしているし![]()
![]()
ファイルサイズが大きい場合は他の方法を考えたほうが良さそう
本当に実験的な場面で活躍できるのかっ![]()
![]()
![]()
次回
DatasetクラスのカスタマイズはDataLoaderと合わせて威力を発揮するようです。次回はDataLoderクラスについて実験を行ってみたいと思案中🌵
目次
参考
- PyTorchのチュートリアル: Fashion-MNIST を題材にして解説してあります。Further Reading まで読み進めるとどんどん深みにハマります😅
- PyTorchのドキュメント:カスタムしなくても標準で役立つDatasetクラスが既に用意されています。さすがです🍀
- PyTorch Dataset と DataLoader の使い方: とてもコンパクトにまとまっています。なにより読みやすい気がします。
- 【実践基礎6】PyTorch の Dataset と DataLoader をわかりやすく解説: 図解付きでわかりやすく解説されています。
- 【PyTorch】自作Datasetの作り方を完全解説: 機械学習と情報技術というサイトの一部。密度の濃い情報量です。凄い量です😆
