--- license: apache-2.0 pipeline_tag: text-generation base_model: - Qwen/Qwen3-0.6B --- # Qwen3 Phoneme (ARPABET) Converter — T2P / P2T(Unsloth + Dolly text-only) このリポジトリ(ノートブック)では、**Qwen3 系モデル**を Unsloth で SFT し、 (1) **Text → ARPABET phonemes(T2P)** と (2) **ARPABET phonemes → Text(P2T)** の 2 タスクを同一モデルで扱えるようにしたものです。 [Brain-to-text '25](https://www.kaggle.com/competitions/brain-to-text-25)用に作成しました。 --- ## 1. 何ができるか(概要) - **T2P (Text to Phoneme)** 英文を ARPABET 音素列(Brain-to-text '25 互換トークン)に変換 - **P2T (Phoneme to Text)** ARPABET 音素列(同トークン)から英文を復元 --- ## 2. 追加トークン仕様(重要) ### 2.1 Control tokens ノートブック内で `tokenizer.add_special_tokens` により追加しています。 - `START_TOKEN` : `<|PHONEME_START|>` - `END_TOKEN` : `<|PHONEME_END|>` - `T2P_TOKEN` : `<|CVT2P_START|>` (Text→Phoneme の開始マーカー) - `P2T_TOKEN` : `<|CVP2T_START|>` (Phoneme→Text の開始マーカー) ### 2.2 Silence token - `SILENCE_TOKEN` : `<|PNM_SIL|>` ### 2.3 ARPABET phoneme tokens(39音素) 以下の 39 音素を **`<|PNM_XX|>` 形式**で追加します。 `AA AE AH AO AW AY B CH D DH EH ER EY F G HH IH IY JH K L M N NG OW OY P R S SH T TH UH UW V W Y Z ZH` > 追加される総トークン数(ノートブックの定義) - Control 4個 + Silence 1個 + Phoneme 39個 = **合計 44 トークン** ### 2.4 テキスト→音素列(`encode_text_to_pnm`)のルール(ノートブック準拠) - `g2p_en` を用いて単語を ARPABET に変換 - ストレス数字(例: `IY1`)は除去して `IY` に正規化 - `PHONEME_SET` に含まれない音素は **捨てる** - 単語間に `SILENCE_TOKEN`(`<|PNM_SIL|>`)を挿入可能 - 必要なら `START_TOKEN` / `END_TOKEN` を付与 - 返り値は **トークンを基本的に結合した1文字列**(ノートブックは `return "".join(tokens)`) --- ## 3. プロンプト仕様(T2P / P2T) ### 3.1 T2P(Text → ARPABET phonemes) **User** ``` Convert to ARPABET phonemes: <|CVT2P_START|>{TEXT} ``` **Assistant(教師信号)** ``` {encode_text_to_pnm(TEXT)} ``` ノートブックでは、会話データを以下で生成しています(概念): - user: `Convert to ARPABET phonemes:\n{T2P_TOKEN}{input_text}` - assistant: `encode_text_to_pnm(input_text)` ### 3.2 P2T(ARPABET phonemes → Text) **User** ``` Convert ARPABET phonemes to text: <|CVP2T_START|>{PHONEME_SEQUENCE} ``` **Assistant(教師信号)** ``` {TEXT} ``` ノートブックでは、会話データを以下で生成しています(概念): - user: `Convert ARPABET phonemes to text:\n{P2T_TOKEN}{encode_text_to_pnm(input_text)}` - assistant: `input_text` --- ## 4. トレーニングの流れ(ノートブックの手順) 1) **G2P と音素エンコード関数を定義** - `PHONEME_SET` / `SILENCE_TOKEN` / `START_TOKEN` / `END_TOKEN` / `T2P_TOKEN` / `P2T_TOKEN` - `_normalize_text`, `word_to_arpabet`, `encode_text_to_pnm` 2) **Unsloth でモデル・トークナイザ読み込み** - `FastLanguageModel.from_pretrained(...)` 3) **追加トークンを tokenizer に登録し、埋め込みを拡張** - `tokenizer.add_special_tokens({"additional_special_tokens": add_tokens})` - `model.resize_token_embeddings(len(tokenizer))` 4) **チャットテンプレを指定** - `get_chat_template(..., chat_template="qwen3-instruct")` 5) **Dolly text-only データセットをロード** - `load_from_disk(dir_dolly_textonly)` - 期待カラム: `text_line`(ノートブックでは `ROW_ID="text_line"`) 6) **T2P / P2T の学習用会話テキストを生成して結合** - `formatting_prompts_func_t2p` / `formatting_prompts_func_p2t` - `map` でそれぞれの split に適用 - `concatenate_datasets([t2p, p2t])` - split: `train`, `test`, `eval` を同様に作成 7) **LoRA 設定で PEFT 化** - `FastLanguageModel.get_peft_model(...)` 8) **SFTTrainer で学習(SFTConfig)** - `SFTTrainer(model, tokenizer, train_dataset, eval_dataset, args=SFTConfig(...))` 9) **assistant 応答部分のみを loss 対象にする** - `train_on_responses_only(trainer, instruction_part="<|im_start|>user\n", response_part="<|im_start|>assistant\n")` 10) **学習実行** - `trainer.train()` 11) **保存** - LoRA adapters: `model.save_pretrained(dir_save_lora)` + `tokenizer.save_pretrained(dir_save_lora)` - 16bit merge: `model.save_pretrained_merged(dir_save_model, tokenizer, save_method="merged_16bit")` --- ## 5. ハイパーパラメータ(ノートブック準拠) ### 5.1 モデル読み込み - `max_seq_length`: **2048** - `load_in_4bit`: **False** - `load_in_8bit`: **False** - `full_finetuning`: **True** - `cache_dir`: `dir_cache`(環境依存) - `chat_template`: **"qwen3-instruct"** ### 5.2 LoRA(PEFT) `FastLanguageModel.get_peft_model` の設定: - `r`: **32** - `lora_alpha`: **32** - `lora_dropout`: **0** - `bias`: **"none"** - `use_gradient_checkpointing`: **"unsloth"** - `random_state`: **3407** - `use_rslora`: **False** - `loftq_config`: **None** - `target_modules`: - `q_proj, k_proj, v_proj, o_proj` - `gate_proj, up_proj, down_proj` - `lm_head, embed_tokens` ### 5.3 SFT(SFTTrainer / SFTConfig) - `dataset_text_field`: `"text"` - `per_device_train_batch_size`: **24** - `gradient_accumulation_steps`: **2**(実効バッチ = 48) - `warmup_steps`: **5** - `num_train_epochs`: **2** - `learning_rate`: **5e-5** - `logging_steps`: **50** - `optim`: **"adamw_8bit"** - `weight_decay`: **0.001** - `lr_scheduler_type`: **"cosine"** - `seed`: **3407** - `save_strategy`: **"epoch"** - `report_to`: **"none"** - `eval_dataset`: `ds_eval`(※評価戦略は設定に依存) --- ## 6. 推論(T2P / P2T) ### 6.1 例 ``` from g2p_en import G2p import re import unicodedata from typing import List from IPython.display import display # Brain-to-text '25 のラベル集合に合わせた ARPABET 音素(無音 ' | ' は別扱い) PHONEME_SET = { 'AA','AE','AH','AO','AW','AY','B','CH','D','DH','EH','ER','EY','F','G', 'HH','IH','IY','JH','K','L','M','N','NG','OW','OY','P','R','S','SH','T', 'TH','UH','UW','V','W','Y','Z','ZH' } SILENCE_TOKEN = '<|PNM_SIL|>' START_TOKEN = '<|PHONEME_START|>' END_TOKEN = '<|PHONEME_END|>' T2P_TOKEN = "<|CVT2P_START|>" P2T_TOKEN = "<|CVP2T_START|>" PH_PREFIX = 'PNM_' # 変更したければここを変える _g2p = G2p() def _normalize_text(s: str) -> str: """記号の正規化&不要記号の削除(アポストロフィは残す)""" s = unicodedata.normalize("NFKC", s).replace("’", "'") # ハイフンは空白へ、他の句読点は削除 s = re.sub(r"[-_]+", " ", s) s = re.sub(r"[^A-Za-z0-9'\s]", " ", s) s = re.sub(r"\s+", " ", s).strip() return s def word_to_arpabet(word: str) -> List[str]: """単語 -> ARPABET(ストレス数字除去+未知排除)""" phones = _g2p(word) # 例: ["AY1","M"] cleaned = [] for ph in phones: ph = re.sub(r"\d", "", ph) # IY1 -> IY if ph in PHONEME_SET: cleaned.append(ph) return cleaned def encode_text_to_pnm(text: str, add_start_end: bool = True, insert_silence_between_words: bool = True) -> str: """ テキスト -> <|PNM_XX|> 形式の列(必要なら START/END と無音 ' | ' を付与) """ text = _normalize_text(text) if not text: return f"{START_TOKEN} {END_TOKEN}" if add_start_end else "" words = text.split() tokens: List[str] = [] for i, w in enumerate(words): phones = word_to_arpabet(w) tokens.extend([f"<|{PH_PREFIX}{ph}|>" for ph in phones]) # 単語間に無音 ' | ' を入れる(最後の単語の後には入れない) if insert_silence_between_words and i < len(words) - 1: tokens.append(SILENCE_TOKEN) if add_start_end: tokens = [START_TOKEN] + tokens + [END_TOKEN] return "".join(tokens) from unsloth import FastLanguageModel import torch import os fourbit_models = [ "unsloth/Qwen3-4B-Instruct-2507-unsloth-bnb-4bit", # Qwen 14B 2x faster "unsloth/Qwen3-4B-Thinking-2507-unsloth-bnb-4bit", "unsloth/Qwen3-8B-unsloth-bnb-4bit", "unsloth/Qwen3-14B-unsloth-bnb-4bit", "unsloth/Qwen3-32B-unsloth-bnb-4bit", # 4bit dynamic quants for superior accuracy and low memory use "unsloth/gemma-3-12b-it-unsloth-bnb-4bit", "unsloth/Phi-4", "unsloth/Llama-3.1-8B", "unsloth/Llama-3.2-3B", "unsloth/orpheus-3b-0.1-ft-unsloth-bnb-4bit" # [NEW] We support TTS models! ] # More models at https://huggingface.co/unsloth dir_cache = r"/media/kurogane/kioxia1/cache" model_id = r"/media/kurogane/kioxia1/unsloth/phenome/phenome_qwen3_06_dolly_test1_/lora_model" #"unsloth/Qwen3-0.6B" #"unsloth/Qwen3-4B-Instruct-2507" i_ctx = 2048 b_load_in_4bit = False b_load_in_8bit = False dir_save_base = r"/media/kurogane/kioxia1/unsloth/phenome/phenome_qwen3_06_dolly_test1" dir_output = os.path.join(dir_save_base, "outputs") dir_save_lora = os.path.join(dir_save_base, "lora_model") dir_save_model = os.path.join(dir_save_base, "model_phenome") os.makedirs(dir_output, exist_ok=True) os.makedirs(dir_save_lora, exist_ok=True) os.makedirs(dir_save_model, exist_ok=True) model, tokenizer = FastLanguageModel.from_pretrained( model_name = model_id, max_seq_length = i_ctx, # Choose any for long context! load_in_4bit = b_load_in_4bit, # 4 bit quantization to reduce memory load_in_8bit = b_load_in_8bit, # [NEW!] A bit more accurate, uses 2x memory full_finetuning = True, # [NEW!] We have full finetuning now! # token = "hf_...", # use one if using gated models cache_dir = dir_cache, ) sample = "hello. i'm still in the meeting. how are you?" s_tkns_phenomes = encode_text_to_pnm(sample) print(f"Input: {sample}\nPhenome: {s_tkns_phenomes}") inputs_t2p = tokenizer.apply_chat_template( [ {"role" : "user", "content" : f"Convert to ARPABET phonemes:\n{T2P_TOKEN}{sample}"}, # {"role" : "assistant", "content" : s_tkns_phenomes} ], add_generation_prompt = True, # Must add for generation return_tensors = "pt", tokenize = True, return_dict = True, ).to("cuda") inputs_p2t = tokenizer.apply_chat_template( [ {"role" : "user", "content" : f"Convert ARPABET phonemes to text:\n{P2T_TOKEN}{s_tkns_phenomes}"}, # {"role" : "assistant", "content" : sample} ], add_generation_prompt = True, # Must add for generation return_tensors = "pt", tokenize = True, return_dict = True, ).to("cuda") from transformers import TextStreamer # print(inputs_t2p) print("===T2P===") _ = model.generate( **inputs_t2p, max_new_tokens = 128, # Increase for longer outputs! # Recommended Liquid settings! temperature = 0.3, min_p = 0.15, repetition_penalty = 1.05, # streamer = TextStreamer(tokenizer, skip_prompt = True), ) print(tokenizer.decode(_[0])) print("===P2T===") _ = model.generate( **inputs_p2t, max_new_tokens = 128, # Increase for longer outputs! # Recommended Liquid settings! temperature = 0.3, min_p = 0.15, repetition_penalty = 1.05, # streamer = TextStreamer(tokenizer, skip_prompt = True), ) print(tokenizer.decode(_[0])) ``` > Input: hello. i'm still in the meeting. how are you?
> Phenome: <|PHONEME_START|><|PNM_HH|><|PNM_AH|><|PNM_L|><|PNM_OW|><|PNM_SIL|><|PNM_AY|><|PNM_M|><|PNM_SIL|><|PNM_S|><|PNM_T|><|PNM_IH|><|PNM_L|><|PNM_SIL|><|PNM_IH|><|PNM_N|><|PNM_SIL|><|PNM_DH|><|PNM_AH|><|PNM_SIL|><|PNM_M|><|PNM_IY|><|PNM_T|><|PNM_IH|><|PNM_NG|><|PNM_SIL|><|PNM_HH|><|PNM_AW|><|PNM_SIL|><|PNM_AA|><|PNM_R|><|PNM_SIL|><|PNM_Y|><|PNM_UW|><|PHONEME_END|>
> ===T2P===
> <|im_start|>user
> Convert to ARPABET phonemes:
> <|CVT2P_START|>hello. i'm still in the meeting. how are you?<|im_end|>
> <|im_start|>assistant
> <|PHONEME_START|><|PNM_HH|><|PNM_AH|><|PNM_L|><|PNM_OW|><|PNM_SIL|><|PNM_AY|><|PNM_M|><|PNM_SIL|><|PNM_S|><|PNM_T|><|PNM_IH|><|PNM_L|><|PNM_SIL|><|PNM_IH|><|PNM_N|><|PNM_SIL|><|PNM_DH|><|PNM_AH|><|PNM_SIL|><|PNM_M|><|PNM_IY|><|PNM_T|><|PNM_IH|><|PNM_NG|><|PNM_SIL|><|PNM_HH|><|PNM_AW|><|PNM_SIL|><|PNM_AA|><|PNM_R|><|PNM_SIL|><|PNM_Y|><|PNM_UW|><|PHONEME_END|><|im_end|>
> ===P2T===
> <|im_start|>user
> Convert ARPABET phonemes to text:
> <|CVP2T_START|><|PHONEME_START|><|PNM_HH|><|PNM_AH|><|PNM_L|><|PNM_OW|><|PNM_SIL|><|PNM_AY|><|PNM_M|><|PNM_SIL|><|PNM_S|><|PNM_T|><|PNM_IH|><|PNM_L|><|PNM_SIL|><|PNM_IH|><|PNM_N|><|PNM_SIL|><|PNM_DH|><|PNM_AH|><|PNM_SIL|><|PNM_M|><|PNM_IY|><|PNM_T|><|PNM_IH|><|PNM_NG|><|PNM_SIL|><|PNM_HH|><|PNM_AW|><|PNM_SIL|><|PNM_AA|><|PNM_R|><|PNM_SIL|><|PNM_Y|><|PNM_UW|><|PHONEME_END|><|im_end|>
> <|im_start|>assistant
> hello, i'm still in the meeting. how are you?<|im_end|>