# 模型剪枝：最核心代码
# 需求：BERT 全局非结构化剪枝：对所有 encoder 层注意力权重剪枝 30%，L1 范数
from data_preprocessing import build_dataloader
from model_eval import eval_model
from config import Config
from bert_model import BertClassifierModel
import torch
from torch.nn.utils import prune    # 模型剪枝方法

# 打印权重稀疏度
def compute_sparsity(model):
    """
    计算权重稀疏度
    :param model:
    :return: 稀疏度
    """
    total_params = 0    # Bert预训练模型中参数中个数
    zero_params = 0     # 剪枝后参数值为0的参数个数
    layer_num = len(model.bert_model.encoder.layer)

    for i in range(layer_num):
        weight = model.bert_model.encoder.layer[i].attention.self.query.weight
        total_params += weight.numel()  # 获得权重张量中的参数个数
        zero_params += (weight == 0).sum().item()
    return (zero_params*100.0 / total_params) if total_params > 0 else 0


def print_weights(weight, name, rows=5, cols=5):
    """
    打印权重前n行，前n列
    :param weight: 权重
    :param name: 名称
    :param rows: 前rows行
    :param cols: 前clos列
    :return:
    """
    print(f"\n{name}（前 {rows}x{cols}）：")
    print(weight[:rows, :cols])

if __name__ == '__main__':
    config = Config()
    dev_dataloader = build_dataloader(config.dev_datapath,shuffle=False)
    num_layers = 12 # bert-base-chinese中编码器层有12层

    # 1- 剪枝前
    # 1.1- 加载训练好的模型
    model = BertClassifierModel().to(device=config.device)
    model.load_state_dict(torch.load(config.before_prune_path))
    # 1.2- 验证模型
    f1score, accuracy, precision, recall = eval_model(model)
    print(f"剪枝前，f1score={f1score}，accuracy={accuracy}，precision={precision}，recall={recall}")
    # 1.3- 打印参数信息
    zero_param_rate = compute_sparsity(model)
    print(f"剪枝前 参数值为0的占比{zero_param_rate}")
    # 只对编码器层中第一层的query的权重进行抽样展示
    print_weights(weight=model.bert_model.encoder.layer[0].attention.self.query.weight,name="剪枝前：权重")


    # 2- 【理解】剪枝中
    # 2.1- 规定对Bert模型中什么层的什么参数进行剪枝
    """
        代码解释：
            1- before_prune_model.bert_model.encoder.layer[layer_index].attention.self：
                对模型中 12个编码器层 中的 多头自注意力子层 进行剪枝
            2- multi_self_atten.query、key、value 多头自注意力子层 中的 q、k、v 的 权重w 进行剪枝
    """
    parameters_to_prune = []
    for layer_index in range(num_layers):
        multi_self_atten = model.bert_model.encoder.layer[layer_index].attention.self
        parameters_to_prune.extend([
            (multi_self_atten.query,"weight"),
            (multi_self_atten.key,"weight"),
            (multi_self_atten.value,"weight")
        ])

    # 2.2- 进行全局非结构化剪枝
    """
        参数解释：
            parameters：剪枝范围。也就是规定对Bert模型中什么层的什么参数进行剪枝
            pruning_method：剪枝方式
            amount：剪枝的参数情况。有两种类的参数值，如下
                整数：表示具体对多少个参数剪枝
                小数：表示对Bert模型中多大比例的参数进行剪枝。推荐
    """
    prune.global_unstructured(
        parameters=parameters_to_prune,
        pruning_method=prune.L1Unstructured,
        amount=0.3
    )

    # 2.3- 固化剪枝后的模型：将剪枝后的权重持久化存储到模型结构中
    for param_name,param_type in parameters_to_prune:
        prune.remove(param_name,param_type)

    # 3- 剪枝后
    # 3.1- 验证模型
    f1score, accuracy, precision, recall = eval_model(model)
    print(f"剪枝后，f1score={f1score}，accuracy={accuracy}，precision={precision}，recall={recall}")

    # 3.2- 保存剪枝后的模型
    torch.save(model.state_dict(),config.after_prune_path)

    # 3.3- 打印参数信息
    zero_param_rate = compute_sparsity(model)
    print(f"剪枝后 参数值为0的占比{zero_param_rate}")
    # 只对编码器层中第一层的query的权重进行抽样展示
    print_weights(weight=model.bert_model.encoder.layer[0].attention.self.query.weight, name="剪枝后：权重")

