Иногда LLMки путаются в языке, на котором они отвечают (П維чему нейро考ети дел思ют так). Так почему бы не помочь им забыть про языки, в которых мы не заинтересованы.

Как мы знаем, LLMки работают с токенами: переводят куски слов в многомерные вектора, много раз перемножают их на свои веса, складывают друг с другом, а потом по последнему вектору, вобравшему в себя весь контекст, вычисляют следующий токен. Соответственно, не должно быть затруднительным запретить вычислять конкретные токены из языков, которые нам не нужны.
Давайте попробуем препарировать маленькую модельку Qwen3.5-0.8B-Q8_0.gguf и поправить её.
import ggufreader = gguf.GGUFReader("./Qwen3.5-0.8B-Q8_0.gguf")tokens = reader.fields.get("tokenizer.ggml.tokens").contents()print(f"Total tokens {len(tokens)}")types = reader.fields.get("tokenizer.ggml.token_type").contents()print(f"Total token types {len(set(types))} : {set(types)}")# Total tokens 248320# Total token types 4 : {1, 3, 4, 5}
Она использует byte-pair encoding токенизатор со словарём в 248320 токенов, разделённым на 4 типа (1 — текст, 3 — <|спец|><|токены|>, 4 — <спец></токены>, 5 — паддинг). Текстовые токены начинаются с id_0=! и доходят до id_248043=ありますか . Давайте определим список токенов, которые мы хотим оставить:
import syskeep_check = regex.compile(r"[" r"[\p{N}\p{P}\p{S}&&\p{Script=Common}]" # Числа, знаки препинания и символы r"\s" # Пробельные символы r"а-яА-ЯёЁ" # Кириллица r"a-zA-Z" # Латинский алфавитr"]", regex.V1)symbols_to_keep = set()for cp in range(sys.maxunicode + 1): if keep_check.fullmatch(chr(cp)): symbols_to_keep.add(chr(cp))print(f"{len(symbols_to_keep)} symbols")# 7994 symbols
Так у нас появился список из 7994 символов. Теперь можно отфильтровать токены LLMки, чтобы они содержали только эти символы.
from gguf.vocab import bytes_to_unicodebyte_decoder = {ch: b for b, ch in bytes_to_unicode().items()}tokens_to_rm = set()for i, t in enumerate(tokens): if types[i] != 1: # Нас интересуют только текстовые токены continue raw = bytes(byte_decoder[ch] for ch in t) try: text = raw.decode("utf-8") if not all((ch in symbols_to_keep) for ch in text): tokens_to_rm.add(i) except UnicodeDecodeError: # Обломки utf-8, пусть будут continue print(f"Remove {len(tokens_to_rm)} tokens") # Remove 100468 tokens
Итого от изначальных 248320 токенов мы удалим 100468. Будем это делать зануляя веса в предпоследнем слое, который занимается декодированием многомерного вектора в logit токена.
for t in reader.tensors: print(t.name)# output_norm.weight# token_embd.weight# blk.0.attn_gate.weight# ... ещё 329 слоёв# blk.24.nextn.shared_head_norm.weight# blk.24.post_attention_norm.weight
Вот этот token_embd.weight нам и нужен. Это общий encode/decode слой, поэтому зануление токенов отразится и на умении LLMки читать тексты на этих языках. Ну что ж, это жертва, на которую я готов пойти.
import numpy as nptoken_embd = next(t for t in reader.tensors if t.name == "token_embd.weight")n_vocab, bytes_per_row = token_embd.data.shapeblock_size = 1*2 + 32*1 # Q8_0: один fp16 масштабный коэффициент + 32 int8n_blocks = bytes_per_row // block_size# будем патчить файл напрямуюtoken_embd_data = np.memmap( gguf_file, dtype=np.uint8, mode="r+", offset=token_embd.data_offset, shape=(n_vocab, n_blocks, block_size))# зануляем fp16 масштабный коэффициентtoken_embd_data[tokens_to_rm, :, 0:2] = 0token_embd_data.flush()del token_embd_data
Теперь попробуем, что у нас получилось. Запустим эту модель в llama.cpp и попробуем добиться от неё иероглифов.
На этом, собственно, всё.
Код целиком
import shutilgguf_file = shutil.copyfile( "./Qwen3.5-0.8B-Q8_0.gguf", "./Qwen3.5-0.8B-Q8_0.latcyr.gguf")import ggufreader = gguf.GGUFReader(gguf_file)tokens = reader.fields.get("tokenizer.ggml.tokens").contents()print(f"Total tokens {len(tokens)}")types = reader.fields.get("tokenizer.ggml.token_type").contents()print(f"Total token types {len(set(types))} : {set(types)}")import sysimport regexkeep_check = regex.compile(r"[" r"[\p{N}\p{P}\p{S}&&\p{Script=Common}]" # Общие числа, знаки препинания и символы r"\s" # Пробельные символы r"а-яА-ЯёЁ" # Кириллица r"a-zA-Z" # Латинский алфавитr"]", regex.V1)symbols_to_keep = set()for cp in range(sys.maxunicode + 1): if keep_check.fullmatch(chr(cp)): symbols_to_keep.add(chr(cp))print(f"{len(symbols_to_keep)} symbols")from gguf.vocab import bytes_to_unicodeBYTE_DECODER = {ch: b for b, ch in bytes_to_unicode().items()}tokens_to_rm = []for i, t in enumerate(tokens): if types[i] != 1: continue raw = bytes(BYTE_DECODER[ch] for ch in t) try: text = raw.decode("utf-8") if not all((ch in symbols_to_keep) for ch in text): tokens_to_rm.append(i) except UnicodeDecodeError: # Обломки utf-8 continueprint(f"Remove {len(tokens_to_rm)} tokens")import numpy as nptoken_embd = next(t for t in reader.tensors if t.name == "token_embd.weight")n_vocab, bytes_per_row = token_embd.data.shapeblock_size = 1*2 + 32*1 # Q8_0: один fp16 масштабный коэффициент + 32 int8n_blocks = bytes_per_row // block_sizetoken_embd_data = np.memmap( gguf_file, dtype=np.uint8, mode="r+", offset=token_embd.data_offset, shape=(n_vocab, n_blocks, block_size))token_embd_data[tokens_to_rm, :, 0:2] = 0 # зануляем fp16 масштабный коэффициентtoken_embd_data.flush()del token_embd_data
ссылка на оригинал статьи https://habr.com/ru/articles/1085742/