【機材協力について】
本記事(および本連載プロジェクト)は、株式会社ハイレゾ様よりGPUのクラウドサービス「GPUSOROBAN」の計算資源(NVIDIA A100 80GB)を無償でご提供いただき、開発・検証を行っています。
Part1はこちらから→https://qiita.com/UT_AI_CLUB/items/fae636ab3a9b75ebf930
0.はじめに
こんにちは!東京大学文科3類2年で、東大AI研究会 代表の青木宏太朗です。
現在GPTモデルをフルスクラッチで開発する「SGT(Scratch Generative Transformer)プロジェクト」を進めています。
この記事ではPart2【データ準備編】として、AIの訓練に欠かせない学習データを用意する過程を余すことなくお伝えします。
私たち東大AI研究会は東大で活動するサークルで、1年かけて0からGPT型モデルを開発するために、毎週水曜日に勉強会を開催しています。勉強会では初心者でも理解しやすいスライドと、充実したカリキュラムを用いて開発を進めています。
株式会社ハイレゾ様は自社でGPUデータセンターを運営し「GPUSOROBAN」というGPUのクラウドサービスを提供する会社です。高性能GPUは非常に高価で、私たちのような学生サークルが簡単に手を出せるような代物ではありません。GPUSOROBANではクラウド上で必要な期間・量だけGPUを借りることができるため、初期投資を抑えてGPUを活用し開発を行うことができます。
1.使用する学習データの考え方
①使用するデータの量
SGTプロジェクトでは、25億パラメータのモデルを使用します。Chinchillaのスケーリング則で示された、パラメータ数に対して約20倍の学習トークンを用いるという目安を参考にし、最低500億トークンの学習を目標とします。 実際に学習するデータの配分は以下のようになります。
②データのポートフォリオ
SGTプロジェクトのモデル(25億パラメータ)の事前学習には、総計600億tokenを用意しました。
| データセット | ドメイン | トークン数(十億) | トークン数全体割合 (%) | ライセンス |
|---|---|---|---|---|
| FineWeb-Edu | 一般Web(英) | 15.3 | 25.5 | ODC-BY |
| RedPajama-ArXiv | 論文(英) | 1.0 | 1.7 | 各論文の個別ライセンス(本記事末尾を参照) |
| FineWeb-2-Edu-japanese | 一般Web | 25.8 | 42.9 | ODC-BY |
| llm-jp-corpus-v4-en_wiki | 百科事典(英) | 4.1 | 6.9 | CC BY-SA 3.0 |
| llm-jp-corpus-v4-ja_wiki | 百科事典 | 2.3 | 3.8 | CC BY-SA 3.0 |
| llm-jp-corpus-v4-ja_kaken | 学術・論文 | 2.2 | 3.7 | CC BY 4.0 |
| llm-jp-corpus-v4-ja_patent | 学術・論文 | 2.9 | 4.8 | CC BY 4.0 |
| llm-jp-corpus-v4-code_olmo-starcoder | コード | 6.4 | 10.7 | ODC-BY |
※割合は小数第2位を四捨五入しているため、合計が100%にならない場合があります。
2.データ収集・前処理の流れ
今回は訓練用に日本語の高品質なデータセットを探すところから始めました。データセットの選定には以下の観点を特に意識しました。
・文章として成立しているのか
ネット掲示板の見出しやWebサイトのタグなど、文章として成立していないフレーズが多く含まれているデータセットは除外しました。
・データに偏りがないか
品質の良いテキストデータであっても、内容に偏りがあると、モデルの汎化性能が下がってしまいます。論文や特許の文章だけでなく、日常的な話題も含めるようにしました。
・ライセンスに問題はないか
使用するデータセットのライセンスを確認し、問題ないものを選びました。
上記の観点から、llm-jp-corpus-v4のサブセットを多用します。
今回はリソースと時間の制約上、高度な重複削除は見送りました。その代わり、元からクリーンなllm-jp-corpusを中心に選定することで品質を担保する戦略をとりました。
3.トークナイズ
SGTプロジェクトで使用する東大AI研究会作成のモデルでは、トークナイザーにGPT-2を使用しているため、ここでも同じくトークナイズにはGPT-2を使用します。
大量のデータをトークナイズするのには並列処理は欠かせません。ハイレゾ様にご提供いただいたA100は本番の学習に温存するため、前処理・トークナイズのフェーズはGoogle ColabのPay As You Go環境を並列で回して対応しました。Hugging Faceにまとめたテキストデータを、Google Colab上でトークナイズしました。
トークナイズには以下のコードをGeminiに書いてもらい、使用しました。並列処理を行いつつ、メモリやローカルストレージの逼迫を防ぐトークナイズ用スクリプトを紹介します。本番環境のGPUを温存するため、CPU環境で事前に並列実行し、Hugging Faceへ逐次アップロードし、アップロード済みの一時ファイルを削除することで、ローカルストレージの使用量を抑える仕様です。
コード①:メモリ・ストレージ不足を回避する並列トークナイズ (クリックで展開)
# ja_patentなどをトークナイズする際のセッションRAM枯渇を回避する最適化コード。
# 工夫ポイント:アップロード完了後にローカルの一時ファイルを即座に物理削除し、
# ガベージコレクションを走らせることで、処理が進んでもメモリが肥大化しない設計にしています。
!pip install datasets transformers tqdm tiktoken huggingface_hub hf_transfer zstandard
import os
os.environ["HF_XET_HIGH_PERFORMANCE"] = "1"
import gc
import time
import json
import gzip
import numpy as np
from transformers import AutoTokenizer
from concurrent.futures import ThreadPoolExecutor
from tqdm.auto import tqdm
from google.colab import userdata
from huggingface_hub import HfApi, hf_hub_download, list_repo_files
# ==========================================
# 1. 各種設定項目(※ご自身の環境に合わせて変更してください)
# ==========================================
TOKENIZER_NAME = "gpt2"
TOKENS_PER_FILE = 100_000_000
# ⚠️ セキュリティのため、ご自身のHugging Faceユーザー名に変更してください
HF_USERNAME = "your_username_here"
SOURCE_REPO_ID = f"{HF_USERNAME}/your_source_repo"
DEST_REPO_ID = f"{HF_USERNAME}/your_dest_repo"
# 処理したい「元データのフォルダ名」: 「保存先のフォルダ名」のペアを記述します
TARGET_TARGETS = {
"ja_patent": "ja_patent_tokens",
# "en_wiki": "en_wiki_tokens", # 他のデータセットを処理する場合は追加
}
LOCAL_BASE_DIR = "./local_cache_chunks"
BATCH_SIZE = 500
CPU_WORKERS = os.cpu_count()
IO_WORKERS = 1 # API制限と競合を防ぐため、通信は1スレッドで確実に処理
# ==========================================
# 2. 認証とHugging Face APIの準備
# ==========================================
try:
HF_TOKEN = userdata.get('HF_TOKEN')
except userdata.SecretNotFoundError:
print("❌ エラー: Colabのシークレットに 'HF_TOKEN' が見つかりません。")
raise
api = HfApi(token=HF_TOKEN)
try:
api.create_repo(repo_id=DEST_REPO_ID, repo_type="dataset", private=True, exist_ok=True)
except Exception:
pass
print(f"トークナイザ '{TOKENIZER_NAME}' を読み込み中...")
tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_NAME, token=HF_TOKEN)
eos_token_id = tokenizer.eos_token_id if tokenizer.eos_token_id is not None else 50256
# ==========================================
# 3. 関数定義
# ==========================================
def tokenize_batch(texts):
start_time = time.time()
encodings = tokenizer(texts, add_special_tokens=False, truncation=False)
flat_tokens = []
for ids in encodings['input_ids']:
flat_tokens.extend(ids)
if not ids or ids[-1] != eos_token_id:
flat_tokens.append(eos_token_id)
return flat_tokens, time.time() - start_time
# 🌟 ファイル単体をHFに直接アップロードし、成功したらローカルから即削除してRAMを解放する関数
def upload_file_and_purge(filepath, path_in_repo, is_state=False):
try:
if not is_state:
print(f"\n☁️ [HF送信開始] {path_in_repo} をアップロード中...")
api.upload_file(
path_or_fileobj=filepath,
path_in_repo=path_in_repo,
repo_id=DEST_REPO_ID,
repo_type="dataset"
)
# 🌟 アップロードが成功したら、ローカルの一時ファイルを即座に物理削除してメモリを空ける
if os.path.exists(filepath):
os.remove(filepath)
if not is_state:
print(f"☁️ [HF同期完了] {path_in_repo} (ローカルキャッシュ削除済)")
gc.collect()
except Exception as e:
print(f"\n❌ アップロードエラー ({path_in_repo}): {e}")
def stream_gz_lines(local_gz_path):
with gzip.open(local_gz_path, 'rt', encoding='utf-8', errors='ignore') as f:
for line in f:
if line.strip():
try:
data = json.loads(line)
text = data.get("text", "")
if text:
yield text
except json.JSONDecodeError:
if line.strip():
yield line.strip()
# ==========================================
# 4. メインループ
# ==========================================
print("🔄 ソースリポジトリからファイル一覧を取得しています...")
all_files = list_repo_files(repo_id=SOURCE_REPO_ID, repo_type="dataset", token=HF_TOKEN)
with ThreadPoolExecutor(max_workers=CPU_WORKERS) as cpu_executor, \
ThreadPoolExecutor(max_workers=IO_WORKERS) as io_executor:
for src_folder, dest_folder in TARGET_TARGETS.items():
print(f"\n=======================================================")
print(f"📁 フォルダ開始: {src_folder} ➔ 保存先: {dest_folder}")
print(f"=======================================================")
folder_local_dir = os.path.join(LOCAL_BASE_DIR, dest_folder)
os.makedirs(folder_local_dir, exist_ok=True)
gz_files = sorted([f for f in all_files if f.startswith(f"{src_folder}/") and f.endswith(".gz")])
if not gz_files:
continue
start_file_name = gz_files[0]
start_row = 0
file_index = 1
state_filename = "state.json"
state_path_in_repo = f"{dest_folder}/{state_filename}"
try:
local_state_path = hf_hub_download(
repo_id=DEST_REPO_ID,
filename=state_path_in_repo,
repo_type="dataset",
token=HF_TOKEN
)
with open(local_state_path, 'r') as f:
state = json.load(f)
start_file_name = state.get("current_file", gz_files[0])
start_row = state.get("processed_rows", 0)
file_index = state.get("file_index", 1)
print(f"🔄 不死鳥セーブ確認: {start_file_name} の {start_row:,} 行目から再開。")
except Exception:
print("🆕 新規開始します。")
buffer_tokens = []
total_tokens_processed = 0
cpu_futures = []
total_download_time = total_calc_time = total_save_time = 0.0
if start_file_name in gz_files:
files_to_process = gz_files[gz_files.index(start_file_name):]
else:
files_to_process = gz_files
start_row = 0
for file_path in files_to_process:
print(f"\n📥 ダウンロード中: {file_path}")
dl_start = time.time()
local_gz_path = hf_hub_download(repo_id=SOURCE_REPO_ID, filename=file_path, repo_type="dataset", token=HF_TOKEN)
total_download_time += (time.time() - dl_start)
line_generator = stream_gz_lines(local_gz_path)
processed_rows = 0
if file_path == start_file_name and start_row > 0:
for _ in range(start_row):
next(line_generator, None)
processed_rows = start_row
start_row = 0
batch_texts = []
for text in line_generator:
batch_texts.append(text)
processed_rows += 1
if len(batch_texts) >= BATCH_SIZE:
future = cpu_executor.submit(tokenize_batch, batch_texts)
cpu_futures.append(future)
batch_texts = []
while len(cpu_futures) >= int(CPU_WORKERS * 1.5):
completed_future = cpu_futures.pop(0)
batch_tokens, calc_time = completed_future.result()
total_calc_time += calc_time
buffer_tokens.extend(batch_tokens)
while len(buffer_tokens) >= TOKENS_PER_FILE:
chunk_to_save = buffer_tokens[:TOKENS_PER_FILE]
buffer_tokens = buffer_tokens[TOKENS_PER_FILE:]
filename = f"chunk_{file_index:04d}_{TOKENS_PER_FILE//1_000_000}M_gpt2.npy"
output_filepath = os.path.join(folder_local_dir, filename)
save_start = time.time()
np_chunk = np.array(chunk_to_save, dtype=np.uint16)
np.save(output_filepath, np_chunk)
total_save_time += (time.time() - save_start)
print(f"✅ ローカル一時保存: [{dest_folder}] {filename}")
local_state_path = os.path.join(folder_local_dir, f"state_{src_folder}.json")
state_data = {
"current_file": file_path,
"processed_rows": processed_rows,
"file_index": file_index + 1
}
with open(local_state_path, 'w') as f:
json.dump(state_data, f)
# 🌟 .npyファイルを直接HFの指定フォルダへアップロード指示(完了後にローカルから自動消去)
io_executor.submit(upload_file_and_purge, output_filepath, f"{dest_folder}/{filename}", is_state=False)
io_executor.submit(upload_file_and_purge, local_state_path, state_path_in_repo, is_state=True)
file_index += 1
total_tokens_processed += TOKENS_PER_FILE
total_download_time = total_calc_time = total_save_time = 0.0
del np_chunk, chunk_to_save
gc.collect()
if batch_texts:
future = cpu_executor.submit(tokenize_batch, batch_texts)
cpu_futures.append(future)
if os.path.exists(local_gz_path):
os.remove(local_gz_path)
# 最終回収フェーズ
while len(cpu_futures) > 0:
completed_future = cpu_futures.pop(0)
batch_tokens, calc_time = completed_future.result()
buffer_tokens.extend(batch_tokens)
while len(buffer_tokens) >= TOKENS_PER_FILE:
chunk_to_save = buffer_tokens[:TOKENS_PER_FILE]
buffer_tokens = buffer_tokens[TOKENS_PER_FILE:]
filename = f"chunk_{file_index:04d}_{TOKENS_PER_FILE//1_000_000}M_gpt2.npy"
output_filepath = os.path.join(folder_local_dir, filename)
np_chunk = np.array(chunk_to_save, dtype=np.uint16)
np.save(output_filepath, np_chunk)
local_state_path = os.path.join(folder_local_dir, f"state_{src_folder}.json")
state_data = {"current_file": gz_files[-1], "processed_rows": processed_rows, "file_index": file_index + 1}
with open(local_state_path, 'w') as f:
json.dump(state_data, f)
print(f"✅ 最終回収保存: {filename}")
io_executor.submit(upload_file_and_purge, output_filepath, f"{dest_folder}/{filename}", is_state=False)
io_executor.submit(upload_file_and_purge, local_state_path, state_path_in_repo, is_state=True)
file_index += 1
total_tokens_processed += TOKENS_PER_FILE
del np_chunk, chunk_to_save
gc.collect()
# 最終端数(Remainder)の処理
if buffer_tokens:
print(f"\n📦 {src_folder} の最終端数データ({len(buffer_tokens):,} トークン)を処理中...")
filename = f"chunk_{file_index:04d}_remainder_gpt2.npy"
output_filepath = os.path.join(folder_local_dir, filename)
np_chunk = np.array(buffer_tokens, dtype=np.uint16)
np.save(output_filepath, np_chunk)
local_state_path = os.path.join(folder_local_dir, f"state_{src_folder}.json")
state_data = {"current_file": gz_files[-1], "processed_rows": processed_rows, "file_index": file_index + 1}
with open(local_state_path, 'w') as f:
json.dump(state_data, f)
print(f"✅ 端数保存: {filename}")
io_executor.submit(upload_file_and_purge, output_filepath, f"{dest_folder}/{filename}", is_state=False)
io_executor.submit(upload_file_and_purge, local_state_path, state_path_in_repo, is_state=True)
total_tokens_processed += len(buffer_tokens)
del np_chunk
gc.collect()
print(f"\n✨ フォルダ {src_folder} の全タスクが完了。合計トークン: {total_tokens_processed:,}")
print("\n☁️ バックグラウンドの最終アップロードの完了を待機しています...")
print(f"\n🎉 RAMの安全を確保し、使い慣れた .npy 形式での一括全自動トークナイズが完全終了しました!")
なんやかんやで2日間くらいかけてトークナイズを行いました。
トークナイズ中はちゃんとHugging Faceにアップロードできているのか、途中でランタイムの切断が起きていないかなどが不安でした。
すべてトークナイズが終わり一安心..........と思いきや、トークナイズの段階でシャッフルを行わなかったため、各データセットがジャンルごとに固まった状態になっていました。このまま学習させると破滅的忘却を引き起こすリスクがあります。そこで、トークンを保存したHugging Face上の .npy ファイルをストリーム形式で読み込み、EOSトークンによる文書境界を維持しながら、データセット間で混合・再構成して、トークンのシャッフルを行いました。
そのためのコードもGeminiに書いてもらいました。Gemini本当にありがとう。
コード②:文書境界を維持したストリーム混合・シャッフル (クリックで展開)
# トークンシャッフル本番。
#Hugging Faceから.npyファイルを1つずつ取得し、処理後にローカルファイルを削除するこ #とで、全データを一度に保存することなく順次処理します。
import numpy as np
import os
import random
import time
from collections import deque
from huggingface_hub import hf_hub_download, list_repo_files, HfApi
from google.colab import userdata
# ==========================================
# 1. 本番用カスタマイズ設定
# ==========================================
try:
HF_TOKEN = userdata.get('HF_TOKEN')
except userdata.SecretNotFoundError:
print("❌ エラー: Colabのシークレットに 'HF_TOKEN' が見つかりません。")
raise
# ⚠️ セキュリティのため、ご自身のHugging Faceユーザー名・リポジトリ名に変更してください
# 読み込み元リポジトリ
SOURCE_REPO_ID = "your_username_here/your_source_repo"
# 保存先リポジトリ(同じリポジトリの別のフォルダに保存します)
DEST_REPO_ID = "your_username_here/your_dest_repo"
DEST_FOLDER = "perfect_mixed_tokens_20b"
# 🌟 本番用:作成する総ファイル数と、途中再開の設定
# 1ファイル約1億(100M)トークン。600個で約60Bトークン分になります。
TARGET_NUM_OUTPUT_FILES = 600
TARGET_TOKENS_PER_FILE = 100_000_000
# ⚠️ もしColabが途中で落ちたら、ここの数字を「最後に成功した次の番号」に変えて実行!
START_FILE_IDX = 1
MIXTURE_RATIO = {
"fineweb_edu_en_tokens": 15.3,
"redpajama_arxiv_tokens": 1.0,
"fineweb_edu_ja_tokens": 25.8,
"en_wiki_tokens": 4.1,
"ja_wiki_tokens": 2.3,
"ja_kaken_tokens": 2.2,
"ja_patent_tokens": 2.9,
"code_olmo_starcoder_tokens": 6.4,
}
#データの混合比率を記録しておく。ここで正しい混合比率にしておかないと、シャッフル前とシャッフル後でデータの割合が変わってしまう。最後まで学習トークンが一部のジャンルに偏らないようにするため、トークンの絶対量の少ないジャンルが不足しないように、もとの比率そのままにシャッフルする。
EOS_ID = 50256
api = HfApi(token=HF_TOKEN)
# ==========================================
# 2. リモート直結ストリームクラス(本番仕様)
# ==========================================
class RemoteGenreStream:
def __init__(self, repo_id, folder_name, hf_token, all_repo_files):
self.repo_id = repo_id
self.folder_name = folder_name
self.hf_token = hf_token
self.files = sorted([f for f in all_repo_files if f.startswith(f"{folder_name}/") and f.endswith(".npy")])
if not self.files:
raise FileNotFoundError(f"❌ '{folder_name}/' に .npy ファイルが見つかりません。")
self.current_file_idx = 0
self.document_queue = deque()
self.epoch = 1
print(f" 🌐 [接続OK] {folder_name}: {len(self.files)} 個のファイルを認識 ({(len(self.files)*100_000_000)/1e9:.1f}B トークン分)")
def _load_next_file(self):
if self.current_file_idx >= len(self.files):
print(f" 🔄 [{self.folder_name}] が1周しました。シャッフルして第{self.epoch + 1}エポックへ入ります。")
random.shuffle(self.files)
self.current_file_idx = 0
self.epoch += 1
remote_filepath = self.files[self.current_file_idx]
self.current_file_idx += 1
# 1. ダウンロード
local_path = hf_hub_download(
repo_id=self.repo_id,
filename=remote_filepath,
repo_type="dataset",
token=self.hf_token
)
# 2. メモリに安全に展開(.copy() が超重要:ファイルロックを解除するため)
tokens = np.load(local_path).copy()
# 3. 直ちにローカルファイルを削除してColabの容量パンクを防ぐ
if os.path.exists(local_path):
os.remove(local_path)
split_indices = np.where(tokens == EOS_ID)[0] + 1
if len(split_indices) > 0 and split_indices[-1] == len(tokens):
split_indices = split_indices[:-1]
self.document_queue.extend(np.split(tokens, split_indices))
def fetch_documents(self, target_tokens):
fetched_docs = []
fetched_tokens = 0
while fetched_tokens < target_tokens:
if not self.document_queue:
self._load_next_file()
doc = self.document_queue.popleft()
fetched_docs.append(doc)
fetched_tokens += len(doc)
return fetched_docs
# ==========================================
# 3. 本番処理の初期化
# ==========================================
print(f"🔍 Hugging Face リポジトリからファイル構造を取得中...")
try:
all_repo_files = list_repo_files(repo_id=SOURCE_REPO_ID, repo_type="dataset", token=HF_TOKEN)
except Exception as e:
print(f"❌ アクセス失敗: {e}")
exit()
print("\n🚀 データストリームを構築します...")
active_streams = {}
valid_weights = {}
for folder, weight in MIXTURE_RATIO.items():
try:
active_streams[folder] = RemoteGenreStream(SOURCE_REPO_ID, folder, HF_TOKEN, all_repo_files)
valid_weights[folder] = weight
except FileNotFoundError as e:
print(e)
if not active_streams:
exit()
total_weight = sum(valid_weights.values())
# ==========================================
# 4. メインループ(生成・アップロード・削除)
# ==========================================
print(f"\n🔥 本番シャッフルを開始します!({START_FILE_IDX}個目 〜 {TARGET_NUM_OUTPUT_FILES}個目)")
print("="*60)
for file_idx in range(START_FILE_IDX, TARGET_NUM_OUTPUT_FILES + 1):
step_start_time = time.time()
mixed_documents = []
print(f"⏳ [{file_idx}/{TARGET_NUM_OUTPUT_FILES}] データを抽出・混合中...")
# 1. 比率通りに抽出
for folder, stream in active_streams.items():
target_tokens = int(TARGET_TOKENS_PER_FILE * (valid_weights[folder] / total_weight))
docs = stream.fetch_documents(target_tokens)
mixed_documents.extend(docs)
# 2. 完璧なランダムシャッフル
random.shuffle(mixed_documents)
final_tokens = np.concatenate(mixed_documents)
# 3. 一時保存
output_filename = f"perfect_mixed_{file_idx:04d}_gpt2.npy"
temp_filepath = f"./{output_filename}"
np.save(temp_filepath, final_tokens)
# 4. HFへアップロード
hf_path = f"{DEST_FOLDER}/{output_filename}"
print(f" ☁️ {output_filename} をHugging Faceにアップロード中...")
try:
api.upload_file(
path_or_fileobj=temp_filepath,
path_in_repo=hf_path,
repo_id=DEST_REPO_ID,
repo_type="dataset"
)
except Exception as e:
print(f" ❌ アップロードに失敗しました。処理を一時停止します: {e}")
break # 失敗したらループを抜けてファイルを保持する
# 5. 一時ファイルを削除(お掃除)
if os.path.exists(temp_filepath):
os.remove(temp_filepath)
elapsed = time.time() - step_start_time
print(f" ✅ 完了! (総トークン: {len(final_tokens):,} / 処理時間: {elapsed:.1f}秒)\n")
print("\n🎉 本番シャッフルの全行程が完了(または指定地点まで到達)しました!")
<details
4.データ品質で工夫した点
今回は、あらかじめ品質のいいデータを集めました。しかし、当初はどのようなデータが言語モデルの学習に適しているのか、十分に理解できていませんでした。とにかく論文や特許など、格式高い文章をたくさん学習させようとしました。そして実際にトークナイズすると、特許関連のデータが全体の半分近くになってしまいました!
これでは何でもかんでも特許文のように返してしまうモデルになってしまうということは、初心者の目にも明らかでした。 ここでFineWeb-Edu(https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese )のような幅広いジャンルの文章を含むデータセットの割合を増やしました。
今回はデータを集めるだけで、特別な前処理を加えることはできませんでしたが、きちんとデータセットの内容を見て、性的・暴力的な内容が多く含まれるデータセットは可能な範囲で使用を避けました。
5.学習データ設計上の課題
①データの比率
今回は、英語約34%、日本語約55%、コード約11%の割合でデータを用意しました。私が以前学習させたモデルは英語データのみの学習だったので、複数の言語を混ぜての学習は初めてです。英語、日本語、コードの割合が適切かどうかが不安です。
②データシャッフルの問題
本来はトークナイズ前にテキストのシャッフルを行うべきですが、今回はその工程を完全にすっぽかしてしまいました。データが適切にシャッフルできていないと、過学習と破滅的忘却を繰り返し、モデルの汎化性能が著しく下がってしまいます。今回も汎化性能は高くしていきたいので、今回はトークナイズを行った後にEOSを基にシャッフルを行うという力技で解決しました。
③汎化性能と特化モデル
初めのうちはG検定のシラバスに関連する内容のテキストを100億トークンも集めようとしていましたが、それでは逆にG検定以外のタスクに関する汎化性能が下がってしまうと考えました。言語モデルを訓練させるなら、汎化性能は高くしたい、、、と思い、Part1で設定した「G検定レベルの知識を持つモデルの開発」という指針は崩さずに、学習用データが特定の領域へ極端に偏らないよう配慮しました。基本的には多種多様なコーパスを用いて汎化性能を高めることを優先し、その結果としてG検定の専門知識をどの程度吸収できたかを、独自の評価用データセットで測定する戦略をとっています。G検定に特化させなくても、汎化性能のみでG検定レベルの知識を身につけさせたいですが、事前学習用のデータでどれくらいの性能になるかは、学習が始まらないと分かりません。
こればかりは実際に学習を回してみないとわからないので、次回はハイレゾ様のA100を使った検証の様子をお届けします!
どのような結果になるか、ぜひ記事をフォローしてお待ちください。
関連リンク
株式会社ハイレゾ 公式サイト:https://highreso.jp/
クラウドサービス GPUSOROBAN:https://soroban.highreso.jp/
利用データセットの権利表記と謝辞
本プロジェクトの事前学習データの構築にあたり、以下の素晴らしいオープンデータセットを利用させていただきました。高品質なデータを公開してくださっている開発者・コミュニティの皆様に深く感謝申し上げます。 なお、学習にあたり、各データセットに対して当研究会で「トークナイズ(GPT-2形式)」および「ストリームシャッフルによる結合」の改変を行って使用しています。
FineWeb-Edu / FineWeb-2-Edu-japanese
提供元: HuggingFaceFW(https://huggingface.co/datasets/HuggingFaceFW/fineweb-edu ) hotchpotch(https://huggingface.co/datasets/hotchpotch/fineweb-2-edu-japanese )
ライセンス: ODC-BY 1.0
(※大元であるCommon Crawlの利用規約を遵守し、著作権法第30条の4に基づき当研究会の計算サーバー内でのみ情報解析を実施しています)
llm-jp-corpus-v4 (en_wiki, ja_wiki, ja_kaken, ja_patent, code_olmo-starcoder)
提供元: LLM-jp (Japan LLM Consortium) (https://gitlab.llm-jp.nii.ac.jp/datasets/llm-jp-corpus-v4/-/tree/main?ref_type=heads )
ライセンス: 各サブセットに準拠(CC BY-SA 3.0, CC BY 4.0, ODC-BY 1.0)
RedPajama-Data-1T (ArXiv)
提供元: Together Computer (https://huggingface.co/datasets/togethercomputer/RedPajama-Data-1T)
ライセンス: 各論文の個別ライセンスに準拠(※再頒布不可の規定を遵守するため、本プロジェクトからの公開用データセットからは除外しています)
