言語モデルを作る
はじめに
興味だけで言語モデルをフルスクラッチで作ってみる.
Visual StudioのC#のAI補完のように, コンテキストを踏まえたコード補完は非常に便利だ.
AIアージェントやヴァイブコーディングより便利で実用的だと思っている.
Visual StudioのC/C++にはAI補完はない, 外部サーバにデータを投げる拡張はあるのだが(そして実際便利で使っている), 社内では使えない.
興味も兼ねて言語モデルから自作することにした.
目標
- Fill In the Middle (FIM)に対応した, コード補完専用言語モデル
- ローカルで動作する. 拡張に同梱できる程度のサイズ
- 気に入らなければユーザが無視すればよい程度の精度
- 知識も思考も不要, もっともらしい次を予測出来れば充分
とりあえずやってみた
Gemma-3 270mのconfigをベースに, ボキャブラリサイズを小さくした, パラメータ数250M程度の言語モデルを対象に試行錯誤していた.
lossが3.2前後から下がらないし, 大した成果がないように思えた. 半年ぐらい浪費した.
ChatGPTとやり取りしながら進める
私は機械学習は専門ではないので, 言いなり状態だ. Google検索より早いので裏取りもそこそこに, 750M程度のアーキテクチャに落ち着いた.
300M前後だとそもそも英文法を詰め込むにも足りないらしい.
トーカナイザー
ボキャブラリサイズは6400だが, データセットのクリーニングは修正した. 以前はASCII以外は除去するだけだった.
コメントのエンコード効率も考慮してASCII以外も残し, 代わりに連続した改行・記号を圧縮したりしてみた.
英語テキスト50%, C/C++コード50%の割合で構築した. コード補完が目的なので英語以外は不要なのだ.
train_tokenizer.py
from tokenizers import Tokenizer,normalizers,pre_tokenizers,decoders
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from datasets import load_dataset
import huggingface_hub
import os
import string
import re
import unicodedata
huggingface_hub.login(token=os.environ['HF_TOKEN'])
tokenizer = Tokenizer(BPE(unk_token='<unk>', end_of_word_suffix="</w>", byte_fallback=True))
#tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel()
tokenizer.normalizer = normalizers.NFKC()
tokenizer.decoder = decoders.Sequence(
[
decoders.ByteFallback(),
decoders.BPEDecoder(suffix=tokenizer.model.end_of_word_suffix),
]
)
def clean_text(text):
# Unicode normalize
text = unicodedata.normalize("NFKC", text)
# Normalize line endings
text = text.replace('\r\n', '\n')
text = text.replace('\r', '\n')
# Remove huge blank blocks
text = re.sub(r'\n{4,}', '\n\n\n', text)
# Remove separator-only lines
text = re.sub(r'(?m)^[-_=]{8,}$', '', text)
# Remove control chars (except \n and \t)
text = re.sub(r'[\x00-\x08\x0B-\x1F\x7F]', '', text)
return text
trainer = BpeTrainer(special_tokens=["<unk>", "<bos>", "<eos>", "<pad>", "<mask>", "<|fim_prefix|>", "<|fim_middle|>", "<|fim_suffix|>", "<|fim_pad|>", "<|file_separator|>", "<start_of_image>", "<end_of_image>", "<image_soft_token>"]+[f"<0x{i:02X}>" for i in range(256)], limit_alphabet=4000, vocab_size=64000, max_token_length=10, end_of_word_suffix=tokenizer.model.end_of_word_suffix)
dataset_names = [('agentlans/high-quality-english-sentences', None, None, 'text', 0),('bigcode/the-stack', None, 'data/c', 'content', 10000), ('bigcode/the-stack', None, 'data/c++', 'content', 10000), ('bigcode/the-stack', None, 'data/c-sharp', 'content', 10000), ('bigcode/the-stack', None, 'data/java', 'content', 10000), ('bigcode/the-stack', None, 'data/python', 'content', 10000), ('bigcode/the-stack', None, 'data/ruby', 'content', 10000), ('bigcode/the-stack', None, 'data/go', 'content', 10000)]
#dataset_names = [('agentlans/high-quality-english-sentences', None, None, 'text', 0)]
seed=1141
def dataset_iter():
for name in dataset_names:
dataset = None
key = None
print(name)
if None != name[2]:
count = 0
dataset = load_dataset(name[0], split='train', data_dir=name[2], trust_remote_code=True, streaming=True)
for item in dataset:
count += 1
if 0<name[4] and name[4]<count:
break
yield clean_text(item[name[3]])
else:
count = 0
dataset = load_dataset(name[0], name[1], split='train', trust_remote_code=True, streaming=True)
for item in dataset:
count += 1
if 0<name[4] and name[4]<count:
break
yield clean_text(item[name[3]])
#yield remove_non_ascii(item[name[3]])
tokenizer.train_from_iterator(dataset_iter(), trainer)
tokenizer.save('tokenizer.json')
text = '''
ワガハイは㈱である. 吾輩は猫である。名前はまだない。
'''
tokens = tokenizer.encode(text)
print(tokens.tokens)
print(tokenizer.decode(tokens.ids, skip_special_tokens=False))
アーキテクチャ
flash attention対応を入れつつしていると肥大化した.
full attentionが元のGemma-3 270mより大幅に増えているので推論速度が遅くなりそうだ.
config.json
{
"_sliding_window_pattern": 4,
"architectures": [
"Gemma3ForCausalLM"
],
"model_type": "gemma3_text",
"transformers_version": "5.5.0",
"vocab_size": 64000,
"bos_token_id": 2,
"eos_token_id": 1,
"pad_token_id": 0,
"dtype": "bfloat16",
"hidden_activation": "silu",
"hidden_size": 1280,
"intermediate_size": 5120,
"num_hidden_layers": 28,
"num_attention_heads": 20,
"num_key_value_heads": 5,
"head_dim": 64,
"attention_bias": false,
"attention_dropout": 0.0,
"query_pre_attn_scalar": 64,
"initializer_range": 0.02,
"rms_norm_eps": 1e-6,
"max_position_embeddings": 8192,
"layer_types": [
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention",
"sliding_attention",
"sliding_attention",
"sliding_attention",
"full_attention"
],
"sliding_window": 2048,
"rope_parameters": {
"full_attention": {
"rope_type": "default",
"rope_theta": 100000.0
},
"sliding_attention": {
"rope_type": "default",
"rope_theta": 100000.0
}
},
"attn_logit_softcapping": null,
"final_logit_softcapping": null,
"tie_word_embeddings": true,
"use_cache": true,
"use_bidirectional_attention": false
}
AutoModel
RTX 4070 Ti Superが1枚なので, VRAM16GiBで戦わなければいけない.
初期はHuggingfaceのTransformersのレシピで学習していたが, Nvidia Nemo AutoModelで8bit学習を導入した.
flash attentionと合わせてVRAM16GiBでもぎりぎり動かせるようになった.
8bit Adamはエラーが出て解決できなかった.
まとめ
途中も途中で, いつ学習が終わるんだか, とりあえず以前よりlossは下がっている.
Visual Studio拡張の方はほとんど完成していたりする. こちらも, 独自の特徴の案を試してみたい.