<jupyter_start><jupyter_text>В этом блокноте я запускаю все эксперименты по сжатию модели (а результаты оцениваю в соседнем). Для запуска могут требоваться библиотеки, не указанные в requirements.<jupyter_code>original_path = 'C:/Users/ddale/Downloads/NLP/rusvectores/model.model'
exp_path = 'C:/Users/ddale/Downloads/NLP/compress_ft/'
import gensim
import numpy as np

import gc

import matplotlib.pyplot as plt
%matplotlib inline
import os
import sys

def add_sys_path(p):
    p = os.path.abspath(p)
    if p not in sys.path:
        sys.path.append(p)
add_sys_path('..')
from importlib import reload

import compress_fasttext
reload(compress_fasttext)

import compress_fasttext.utils
reload(compress_fasttext.utils)
from compress_fasttext.utils import getsize

import compress_fasttext.prune
reload(compress_fasttext.prune)
from compress_fasttext.prune import prune_ngrams


import compress_fasttext.navec_like
reload(compress_fasttext.navec_like)

import compress_fasttext.quantization
reload(compress_fasttext.quantization)
from compress_fasttext.quantization import quantize

import compress_fasttext.compress
reload(compress_fasttext.compress)
from compress_fasttext.compress import make_new_fasttext_model, quantize_ft, svd_ft, prune_ft, prune_ft_freq
from compress_fasttext.utils import mb
import compress_fasttext.evaluation
reload(compress_fasttext.evaluation)
from compress_fasttext.evaluation import make_evaluator<jupyter_output><empty_output><jupyter_text>Быстрый выигрыш: fp16 и удаление ненужных матриц<jupyter_code>big_ft = gensim.models.fasttext.FastTextKeyedVectors.load(original_path)
big_ft.adjust_vectors()
print(mb(big_ft))
ev = make_evaluator(big_ft, '../data/toy_corpora/')
print(ev(big_ft))<jupyter_output>0.9999999998843826<jupyter_text>Эксперимент первый: удаляем ненужную матрицу с векторами слов и сокращаем точность до полу-флоата.На значениях векторов это не сказывается *примерно никак*, а модель худеет вдвое.<jupyter_code>ft_fp16 = make_new_fasttext_model(big_ft, big_ft.vectors.astype(np.float16), big_ft.vectors_ngrams.astype(np.float16))
print(mb(ft_fp16))
original_size = mb(big_ft)
fp16_size = mb(ft_fp16)
fp16_sim = ev(ft_fp16)
print(fp16_sim)
ft_fp16.save(exp_path + 'ft_fp16.bin')
ft_fp16_words = prune_ft(big_ft, new_vocab_size=len(big_ft.vocab), new_ngrams_size=1)
ft_fp16_words.vectors_ngrams = ft_fp16_words.vectors_ngrams * 0
print(mb(ft_fp16_words))
print(ev(ft_fp16_words))
ft_fp16_words.save(exp_path + 'ft_fp16_words.bin')
top_vocab, top_vectors = prune_vocab(ft, new_vocab_size=0)
new_ngrams = big_ft.vectors_ngrams.astype(np.float16)

ft_fp16_ngrams = make_new_fasttext_model(
    big_ft,
    new_vectors=top_vectors,
    new_vectors_ngrams=new_ngrams,
    new_vocab=top_vocab,
)
print(mb(ft_fp16_ngrams))
print(ev(ft_fp16_ngrams))
ft_fp16_ngrams.save(exp_path + 'ft_fp16_ngrams.bin')<jupyter_output>1144.410285949707
0.9878330512833536<jupyter_text>Сокращение размерности По существу, тут используется старая добрая матричная декомпозиция (truncated SVD). Широкая матрица из дробных чисел приближенно представляется как произведение двух узких.<jupyter_code>dimred_sizes = []
dimred_sims = []
dimred_dims = [150, 100, 50, 25, 10, 5]

for d in dimred_dims:
    model = svd_ft(big_ft, n_components=d)
    s = mb(model)
    q = vecs_similarity(big_ft, model)
    dimred_sizes.append(s)
    dimred_sims.append(q)
    print(d, s, q)
    model.save(exp_path + 'ft_dimred_{}.bin'.format(d))
    gc.collect()
gc.collect()
plt.plot(dimred_sizes, dimred_sims);
plt.xlabel('model size, mb');
plt.ylabel('old/new vector similarity');<jupyter_output><empty_output><jupyter_text>Квантизация В базовом варианте квантизация - это следующий логичный шаг после перехода от float32 к float16. Теперь мы заменим float'ы в матрице эмбеддингов на int'ы! Но использоваться они будут только для хранения, а при применении эмбеддинга каждому int'у будет сопоставляться дробное число, взятое из специальной матрицы-справочника. Получить такой справочник можно с помощью кластеризации (типа k-means). Идея о кластеризации может навести на следующий шаг: а давайте сопоставим int'ы не отдельным числам-компонентам эмбеддинга, а их последовательностям. Например, если кодироваться будут последовательности из 3 чисел, то 300-мерный вектор можно будет приближенно описать 100 целочисленными кодами. Такой подход называется product quantization, и он позволяет подойти к задаче сокращения размерности с совершенно иной стороны, чем матричная декомпозиция.  ```300 976.6052942276001 0.9934673549560236150 518.0685205459595 0.9764971832428561100 365.2229471206665 0.94925906638925550 212.37737369537354 0.8569715592480625 135.95458698272705 0.725881283425854710 90.10091495513916 0.58382648189863155 74.81635761260986 0.5129448006057942```<jupyter_code>pq_sizes = []
pq_sims = []
pq_dims = [300, 150, 100, 50, 25, 10, 5]

for d in pq_dims:
    model = quantize_ft(big_ft, qdim=d, centroids=255, sample=10_000)
    s = mb(model)
    q = vecs_similarity(big_ft, model)
    pq_sizes.append(s)
    pq_sims.append(q)
    print(d, s, q)
    model.save(exp_path + 'ft_pq_{}.bin'.format(d))
    gc.collect()
plt.plot(dimred_sizes, dimred_sims)
plt.plot(pq_sizes, pq_sims)
plt.scatter([original_size], [1])
plt.scatter([fp16_size], [fp16_sim])
plt.legend(['svd', 'pq', 'original', 'fp16'])

#plt.xscale('log')
plt.xlabel('model size, mb')
plt.ylabel('old/new vector similarity');<jupyter_output><empty_output><jupyter_text>Уменьшение словаря Мы уже попробовали уменьшить матрицы fasttext-модели в ширину (с помощью декомпозиции) и "в глубину" (округляя их значения). Но ведь можно уменьшать матрицы и в длину, просто избавляясь от некоторых строк! Fasttext позволяет делать это относительно безнаказанно, ведь представление незнакомого слова можно построить по его n-граммам, а к самим n-граммам применен hashing trick.<jupyter_code>n_vecs = big_ft.vectors.shape[0]
n_grams = big_ft.vectors_ngrams.shape[0]
print(n_vecs, n_grams)<jupyter_output>164996 2000000<jupyter_text>Честно говоря, тут творится какая-то самая магическая магия, потому что теперь качество модели и размер связаны совсем немонотонно - они зависят от числа коллизий, а оно зависит от размера хэшей... При сжатии модели с коэффициентом 1.0 я получаю тот же самый размер модели, но совершенно ужасную точность.ПОЧЕМУ???<jupyter_code>trim_sizes = []
trim_sims = []
trim_mults = [1.0, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005, 0.002, 0.001]

for d in trim_mults:
    model = trim_ft(big_ft, new_ngrams_size=int(d*n_grams), new_vocab_size=int(d*n_vecs), fp16=True)
    s = mb(model)
    q = vecs_similarity(big_ft, model)
    trim_sizes.append(s)
    trim_sims.append(q)
    print('{:6.6}  {:6.6}  {:6.4}'.format(d, s, q))
    model.save(exp_path + 'ft_trim_{}.bin'.format(d))
    gc.collect()
plt.plot(trim_sizes, trim_sims);
plt.xlabel('model size, mb');
plt.ylabel('old/new vector similarity');<jupyter_output><empty_output><jupyter_text>Отбор n-грам<jupyter_code>big_n = big_ft.vectors_ngrams.shape[0]
small_n = 10_000 

new_to_old_buckets, old_hash_count = compress_fasttext.prune.count_buckets(
    big_ft, list(big_ft.vocab.keys()) , small_n
)
print(big_n)
print(len(old_hash_count))
trimf_sizes = []
trimf_sims = []
trimf_mults = [1.0, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005, 0.002, 0.001]

for d in trimf_mults:
    model = prune_ft_freq(big_ft, new_ngrams_size=int(d*len(old_hash_count)), new_vocab_size=int(d*n_vecs), fp16=True, pq=False)
    s = mb(model)
    q = ev(model)
    trimf_sizes.append(s)
    trimf_sims.append(q)
    print('{:6.6}  {:6.6}  {:6.4}'.format(d, s, q))
    model.save(exp_path + 'ft_prune_freq_{}.bin'.format(d))
    gc.collect()<jupyter_output>1.0  447.942  0.9997
   0.5  224.452  0.9979
   0.2  91.0208  0.9848
   0.1  45.4441  0.9504
  0.05  22.656  0.8855
  0.02  9.1423  0.7675
  0.01  4.56761  0.6627
 0.005  2.27995  0.5778
 0.002  0.891689  0.4594
 0.001  0.444801  0.3591<jupyter_text>Разбираемся в странностях сжатияОдной странностью была бага, которую я посадил при пересортировке словаря. Другая странность - что однобуквенные слова в словарь обученной модели не входят, и оттого совершенно теряются.<jupyter_code>ft = big_ft
gc.collect()
model = gensim.models.fasttext.FastTextKeyedVectors.load(exp_path + 'ft_trim_{}.bin'.format(1.0))
from compress_fasttext.evaluation import cosine, vocabulary_from_files
tv = vocabulary_from_files('../data/toy_corpora/')
len(tv)
lost = set()
for w in tv:
    c = cosine(ft[w], model[w])
    if c < 0.95 and w not in lost:
        lost.add(w)
        print('{:20} {:+4.4} '.format(w, c))<jupyter_output>сетях                +0.8934 
случае               +0.9344 
службы               +0.9065 
ноября               +0.9356<jupyter_text>Из 2 миллионов строк используются, кажется, только 330 тысяч. Вот это расточительность!<jupyter_code>big_n = big_ft.vectors_ngrams.shape[0]
small_n = 10_000 

new_to_old_buckets, old_hash_count = compress_fasttext.prune.count_buckets(
    big_ft, list(big_ft.vocab.keys()) + ['и', 'а', 'у', 'в'], small_n
)
print(big_n)
print(len(old_hash_count))
import pandas as pd
cvs = pd.Series(list(old_hash_count.values())).sort_values(ascending=False)
cvs.quantile([0.5, 0.75, 0.9, 0.95, 0.99])
plt.plot(cvs.values.cumsum());<jupyter_output><empty_output><jupyter_text>Гибридная моделька<jupyter_code>ft_prune_01 = gensim.models.fasttext.FastTextKeyedVectors.load(exp_path + 'ft_trim_{}.bin'.format(0.1))
print(mb(ft_prune_01))
print(vecs_similarity(big_ft, ft_prune_01))
ft_prune_01_pq_100 = quantize_ft(ft_prune_01, qdim=100, centroids=255, sample=None)
print(mb(ft_prune_01_pq_100))
print(vecs_similarity(big_ft, ft_prune_01_pq_100))
ft_prune_01_pq_100.save(exp_path + 'ft_prune_0.1_pq_100.bin')
ft_prune_05 = gensim.models.fasttext.FastTextKeyedVectors.load(exp_path + 'ft_trim_{}.bin'.format(0.5))
print(mb(ft_prune_05))
print(vecs_similarity(big_ft, ft_prune_05))

ft_prune_05_pq_100 = quantize_ft(ft_prune_05, qdim=100, centroids=255, sample=None)
print(mb(ft_prune_05_pq_100))
print(vecs_similarity(big_ft, ft_prune_05_pq_100))
ft_prune_05_pq_100.save(exp_path + 'ft_prune_0.5_pq_100.bin')
ftc2 = prune_ft_freq(big_ft, new_vocab_size=20_000, new_ngrams_size=100_000, pq=True, qdim=100)
print(mb(ftc2))
print(ev(ftc2))
ftc2.save(exp_path + 'ft_freqprune_100K_20K_pq_100.bin')<jupyter_output><empty_output><jupyter_text>Подбор параметров Делаем целый парад гибридных моделек (это можно было бы на самом деле заметно ускорить, копируя словарики, но чё-то мне лень).4 * 3 * 4 = 48 моделек придётся сейчас положить мне на диск. Но ладно.<jupyter_code>from compress_fasttext.prune import prune_ngrams, prune_vocab, count_buckets, RowSparseMatrix
def prune_ft_freq(ft, new_vocab_size=20_000, new_ngrams_size=100_000, fp16=True, pq=True, qdim=100, centroids=255):
    new_to_old_buckets, old_hash_count = count_buckets(ft, list(ft.vocab.keys()), new_ngrams_size=new_ngrams_size)
    id_and_count = sorted(old_hash_count.items(), key=lambda x: x[1], reverse=True)
    ids = [x[0] for x in id_and_count[:new_ngrams_size]]
    top_ngram_vecs = ft.vectors_ngrams[ids]
    if pq and len(top_ngram_vecs) > 0:
        top_ngram_vecs = quantize(top_ngram_vecs, qdim=qdim, centroids=centroids)
    elif fp16:
        top_ngram_vecs = top_ngram_vecs.astype(np.float16)
    rsm = RowSparseMatrix.from_small(ids, top_ngram_vecs, nrows=ft.vectors_ngrams.shape[0])

    top_voc, top_vec = prune_vocab(ft, new_vocab_size=new_vocab_size)
    if pq and len(top_vec) > 0:
        top_vec = quantize(top_vec, qdim=qdim, centroids=centroids)
    elif fp16:
        top_vec = top_vec.astype(np.float16)

    return make_new_fasttext_model(ft, top_vec, rsm, new_vocab=top_voc)

for qdim in [300, 100, 50]:
    for new_ngrams_size in [400_000, 100_000, 50_000]:
        for new_vocab_size in [100_000, 20_000, 5_000, 0]:
            mn = 'ft_freqprune_{}K_{}K_pq_{}.bin'.format(int(new_ngrams_size/1000), int(new_vocab_size/1000), qdim)
            print(mn)
            if os.path.exists(exp_path + mn):
                continue
            ftc = prune_ft_freq(big_ft, new_vocab_size=new_vocab_size, new_ngrams_size=new_ngrams_size, pq=True, qdim=qdim)
            ftc.save(exp_path + mn)<jupyter_output>ft_freqprune_400K_100K_pq_300.bin
ft_freqprune_400K_20K_pq_300.bin
ft_freqprune_400K_5K_pq_300.bin
ft_freqprune_400K_0K_pq_300.bin
ft_freqprune_100K_100K_pq_300.bin
ft_freqprune_100K_20K_pq_300.bin
ft_freqprune_100K_5K_pq_300.bin
ft_freqprune_100K_0K_pq_300.bin
ft_freqprune_50K_100K_pq_300.bin
ft_freqprune_50K_20K_pq_300.bin
ft_freqprune_50K_5K_pq_300.bin
ft_freqprune_50K_0K_pq_300.bin
ft_freqprune_400K_100K_pq_100.bin
ft_freqprune_400K_20K_pq_100.bin
ft_freqprune_400K_5K_pq_100.bin
ft_freqprune_400K_0K_pq_100.bin
ft_freqprune_100K_100K_pq_100.bin
ft_freqprune_100K_20K_pq_100.bin
ft_freqprune_100K_5K_pq_100.bin
ft_freqprune_100K_0K_pq_100.bin
ft_freqprune_50K_100K_pq_100.bin
ft_freqprune_50K_20K_pq_100.bin
ft_freqprune_50K_5K_pq_100.bin
ft_freqprune_50K_0K_pq_100.bin
ft_freqprune_400K_100K_pq_50.bin
ft_freqprune_400K_20K_pq_50.bin
ft_freqprune_400K_5K_pq_50.bin
ft_freqprune_400K_0K_pq_50.bin
ft_freqprune_100K_100K_pq_50.bin
ft_freqprune_100K_20K_pq_50.bin
ft_freqprune_100K_5K_pq_50.bin[...]<jupyter_text>считываем результаты с диска и сравниваемПеренёс этот раздел в evaluate_all.ipynb<jupyter_code>ft_prune_01_pq_100 = gensim.models.fasttext.FastTextKeyedVectors.load(exp_path + 'ft_prune_0.1_pq_100.bin')
ft_freqprune_100K_20K_pq_100 = gensim.models.fasttext.FastTextKeyedVectors.load(exp_path + 'ft_freqprune_100K_20K_pq_100.bin')
format3 = '{:6.6}  {:6.6}  {:6.4}'
dimred_sizes = []
dimred_sims = []
dimred_dims = [150, 100, 50, 25, 10, 5]

for d in dimred_dims:
    model = gensim.models.fasttext.FastTextKeyedVectors.load(exp_path + 'ft_dimred_{}.bin'.format(d))
    s = mb(model)
    q = ev(model)
    dimred_sizes.append(s)
    dimred_sims.append(q)
    print(format3.format(d*1.0, s, q))
    gc.collect()
300 976.6052942276001 0.9934673549560236
150 518.0685205459595 0.9764971832428561
100 365.2229471206665 0.949259066389255
50 212.37737369537354 0.85697155924806
25 135.95458698272705 0.7258812834258547
10 90.10091495513916 0.5838264818986315
5 74.81635761260986 0.5129448006057942
pq_sizes = []
pq_sims = []
pq_dims = [300, 150, 100, 50, 25, 10, 5]

for d in pq_dims:
    model = gensim.models.fasttext.FastTextKeyedVectors.load(exp_path + 'ft_pq_{}.bin'.format(d))
    s = mb(model)
    q = ev(model)
    pq_sizes.append(s)
    pq_sims.append(q)
    print(format3.format(d*1.0, s, q))
    gc.collect()<jupyter_output>300.0  662.427  0.9909
 150.0  352.72  0.9686
 100.0  249.485  0.9377
  50.0  146.25  0.8424
  25.0  94.6319  0.7234
  10.0  63.6613   0.607
   5.0  53.3377  0.5468<jupyter_text>```исходно было вообще что-то бешеное1.0 1280.6667785644531 0.73438343248902140.5 640.8182830810547 0.74754582279795010.2 257.0719566345215 0.78041762477919370.1 128.47385025024414 0.79235247434595480.05 64.23728561401367 0.82178482576245840.02 25.72348403930664 0.76551652700396310.01 12.862011909484863 0.68822147276964050.005 6.431332588195801 0.61392197731912330.002 2.5676469802856445 0.51122797597864970.001 1.2841386795043945 0.4415964335883225```<jupyter_code>trim_sizes = []
trim_sims = []
trim_mults = [1.0, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005, 0.002, 0.001]

for d in trim_mults:
    model = gensim.models.fasttext.FastTextKeyedVectors.load(exp_path + 'ft_trim_{}.bin'.format(d))
    s = mb(model)
    q = ev(model)
    trim_sizes.append(s)
    trim_sims.append(q)
    print(format3.format(d, s, q))
    gc.collect()
plt.figure(figsize=(8,4))

plt.plot(dimred_sizes, dimred_sims)
plt.plot(pq_sizes, pq_sims)
plt.plot(trim_sizes, trim_sims)
plt.plot(trimf_sizes, trimf_sims)

plt.scatter([original_size], [1])
plt.scatter([fp16_size], [fp16_sim])

# plt.scatter([mb(ft_prune_01_pq_100),], [ev(ft_prune_01_pq_100),])

plt.scatter(
    [mb(ft_freqprune_100K_20K_pq_100)], 
    [ev(ft_freqprune_100K_20K_pq_100)]
)
plt.legend(['svd', 'pq', 'prune', 'prune freq', 'original', 'fp16', 'prune freq + pq'], loc='lower right')

plt.xscale('log')
plt.xlabel('model size, mb')
plt.ylabel('cosine similarity')
plt.title('Representation accuracy vs model compression');<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11076809088663868, 'fraction_numerical': 0.12052358607063472, 'mean_word_length': 5.717959352965575, 'pattern_counts': {'":': 0, '<': 51, '<?xml version=': 0, '>': 52, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '491518', 'license': 'MIT', 'n_tokens_mistral': 8509, 'n_tokens_neox': 7440, 'n_words': 1172, 'path': 'experiments/compressions.ipynb', 'repo_name': 'thousandvoices/compress-fasttext', 'size': '16196'}