端侧大模型实践 - 生成预测模型&模型轻量化&端侧部署

为避免模型训练中出现内存异常,先临时增加交换内存:

# 1. 先关闭旧的交换文件(如果之前创建过1GB的)
swapoff /swapfile || true
rm -rf /swapfile || true

# 2. 创建4GB交换文件(bs=1M表示每个块1MB,count=4096表示4096个块=4GB)
dd if=/dev/zero of=/swapfile bs=1M count=4096

# 3. 设置文件权限(仅root可访问,安全要求)
chmod 600 /swapfile

# 4. 格式化交换文件
mkswap /swapfile

# 5. 启用交换文件
swapon /swapfile

# 6. 验证是否生效(查看Swap列,应该显示4.0Gi)
free -h

编写模型训练代码:

import paddle
import os
import random
from paddlenlp.transformers import ErnieTokenizer, ErnieForSequenceClassification
from paddlenlp.data import Stack, Tuple, Pad
from paddle.io import DataLoader, BatchSampler, Dataset

# 核心配置:强制CPU + 最小化内存占用
paddle.set_device("cpu")
paddle.disable_static()
paddle.set_default_dtype("float32")  # 降低精度减少内存

# 1. 极简数据集类(降低内存波动)
class SimpleDataset(Dataset):
    def __init__(self, data):
        self.data = data
    
    def __getitem__(self, idx):
        return self.data[idx]
    
    def __len__(self):
        return len(self.data)

# 仅保留3条核心数据,避免内存占用
raw_data = [
    ("我的订单怎么还没发货", 0),
    ("申请退款多久到账", 1),
    ("产品保质期多久", 2)
]
train_dataset = SimpleDataset(raw_data)
print(f"加载极简训练数据,共 {len(train_dataset)} 条")

# 2. 初始化模型和分词器(忽略权重警告)
tokenizer = ErnieTokenizer.from_pretrained("ernie-3.0-mini-zh")
model = ErnieForSequenceClassification.from_pretrained(
    "ernie-3.0-mini-zh",
    num_classes=3,
    ignore_mismatched_sizes=True  # 关闭权重不匹配警告
)

# 3. 数据预处理(最短文本长度,减少张量大小)
def convert_example(example):
    text, label = example
    inputs = tokenizer(
        text,
        max_len=16,  # 最短文本长度
        padding="max_length",
        truncation=True,
        return_length=False
    )
    return inputs["input_ids"], inputs["token_type_ids"], label

# 提前预处理所有数据,避免加载器中重复计算
processed_data = [convert_example(example) for example in train_dataset]
train_dataset = SimpleDataset(processed_data)

# 4. 数据加载器(批量大小=1,关闭多线程)
batchify_fn = lambda samples, fn=Tuple(
    Pad(axis=0, pad_val=tokenizer.pad_token_id),
    Pad(axis=0, pad_val=tokenizer.pad_token_type_id),
    Stack(dtype="int64")
): fn(samples)

# 精细化批次控制,最低内存占用
sampler = BatchSampler(train_dataset, batch_size=1, shuffle=True)
train_loader = DataLoader(
    dataset=train_dataset,
    batch_sampler=sampler,
    collate_fn=batchify_fn,
    num_workers=0  # 关闭多线程,避免内存泄漏
)

# 5. 训练配置(极简优化器,降低内存)
model.train()
# SGD优化器内存占用远低于Adam
optimizer = paddle.optimizer.SGD(learning_rate=1e-3, parameters=model.parameters())
loss_fn = paddle.nn.CrossEntropyLoss()

# 6. 训练循环(修复no_grad错误,极简逻辑)
epochs = 1
total_loss = 0.0
batch_count = 0

print(f"开始训练 Epoch 1/{epochs}")
for batch in train_loader:
    input_ids, token_type_ids, labels = batch
    
    # 核心修复:删除错误的paddle.no_grad(False),训练需要梯度
    logits = model(input_ids, token_type_ids)
    loss = loss_fn(logits, labels)
    
    # 反向传播 + 优化
    loss.backward()
    optimizer.step()
    optimizer.clear_grad()
    
    # 记录损失
    total_loss += loss.numpy()[0]
    batch_count += 1
    print(f"Batch {batch_count} 训练完成,损失:{loss.numpy()[0]:.4f}")

# 7. 保存模型(仅保存必要文件)
model_dir = "./ernie_demo_model_light"
os.makedirs(model_dir, exist_ok=True)
model.save_pretrained(model_dir, save_config=False)
tokenizer.save_pretrained(model_dir)

# 最终输出
avg_loss = total_loss / batch_count if batch_count > 0 else 0
print("\n==== 训练完全完成 ====")
print(f"模型保存路径:{os.path.abspath(model_dir)}")
print(f"总训练批次:{batch_count},平均损失:{avg_loss:.4f}")
print("提示:权重警告是正常现象,模型已成功训练并保存!")

训练记录如下:
image

验证模型训练结果,创建 validate_model.py 文件,命令:

nano /root/validate_model.py

代码:

import paddle
from paddlenlp.transformers import ErnieTokenizer

# 核心配置:和训练时保持一致
paddle.set_device("cpu")
paddle.disable_static()

# 1. 加载训练好的模型和分词器
# 模型路径:和训练时保存的路径一致(ernie_demo_model_light)
model_path = "./ernie_demo_model_light"
# 加载分词器
tokenizer = ErnieTokenizer.from_pretrained(model_path)
# 加载模型(和训练时的模型结构一致)
from paddlenlp.transformers import ErnieForSequenceClassification
model = ErnieForSequenceClassification.from_pretrained(
    model_path,
    num_classes=3,
    ignore_mismatched_sizes=True
)
# 切换到评估模式(禁用Dropout等训练层)
model.eval()

# 2. 定义标签映射(数字→中文,方便查看)
label_map = {0: "物流咨询", 1: "退款咨询", 2: "产品咨询"}

# 3. 定义验证函数(输入文本,输出分类结果)
def predict(text):
    # 预处理文本(和训练时的逻辑完全一致)
    inputs = tokenizer(
        text,
        max_len=16,
        padding="max_length",
        truncation=True,
        return_length=False
    )
    # 转换为Paddle张量
    input_ids = paddle.to_tensor([inputs["input_ids"]], dtype="int64")
    token_type_ids = paddle.to_tensor([inputs["token_type_ids"]], dtype="int64")
    
    # 模型推理(禁用梯度计算,节省内存)
    with paddle.no_grad():
        logits = model(input_ids, token_type_ids)
        # 获取概率最大的标签
        pred_label = paddle.argmax(logits, axis=1).numpy()[0]
    
    # 返回直观结果
    return label_map[pred_label]

# 4. 验证新文本(选3条未参与训练的文本)
test_texts = [
    "快递到哪了?",       # 预期:物流咨询
    "退款多久能到账?",   # 预期:退款咨询
    "产品怎么使用?"      # 预期:产品咨询
]

# 5. 执行验证并打印结果
print("==== 模型验证结果 ====")
for text in test_texts:
    result = predict(text)
    print(f"输入文本:{text}")
    print(f"模型分类结果:{result}\n"):

验证有问题,但至少跑通了(不过虽然结果不对,这里也先不做追究,因为是要跑通流程,针对细节暂时不关注)

image

因为默认训练后的模型是动态图模型,需要输出静态图模型和移动端模型:

import paddle
import os
from paddlelite.lite import *
from paddlenlp.transformers import ErnieForSequenceClassification

# ====================== 配置项(关键修正:匹配实际分类数)=====================
# 动态图模型路径(你的ernie_demo_model_light)
DYNAMIC_MODEL_PATH = "./ernie_demo_model_light"
# 静态图模型保存路径
STATIC_MODEL_PATH = "./ernie_demo_static/model"
# 移动端模型输出路径
MOBILE_MODEL_PATH = "./ernie_mobile_model"
# 关键修正:改为实际的分类数(从报错看是3分类)
NUM_CLASSES = 3  # 原代码是2,现在改成3,匹配模型参数
# ====================================================

def dynamic2static():
    """第一步:动态图转静态图"""
    # 1. 加载训练好的动态图模型
    # 关键:ignore_mismatched_sizes=True 兼容可能的维度问题(兜底)
    model = ErnieForSequenceClassification.from_pretrained(
        DYNAMIC_MODEL_PATH, 
        num_classes=NUM_CLASSES,
        ignore_mismatched_sizes=True  # 新增:忽略参数维度不匹配(防止漏改分类数)
    )
    model.eval()  # 必须切换到评估模式
    
    # 2. 定义输入规格(和模型输入匹配)
    input_spec = [
        paddle.static.InputSpec(shape=[None, None], dtype="int64", name="input_ids"),
        paddle.static.InputSpec(shape=[None, None], dtype="int64", name="token_type_ids")
    ]
    
    # 3. 导出静态图模型(生成.pdmodel和.pdiparams)
    paddle.jit.save(
        layer=model,
        path=STATIC_MODEL_PATH,
        input_spec=input_spec
    )
    print(f"✅ 静态图模型已生成:")
    print(f"   - {STATIC_MODEL_PATH}.pdmodel")
    print(f"   - {STATIC_MODEL_PATH}.pdiparams")

def static2mobile():
    """第二步:静态图转移动端模型"""
    # 1. 检查静态图文件是否存在
    model_file = f"{STATIC_MODEL_PATH}.pdmodel"
    param_file = f"{STATIC_MODEL_PATH}.pdiparams"
    if not os.path.exists(model_file) or not os.path.exists(param_file):
        raise FileNotFoundError("静态图模型文件不存在,请先运行dynamic2static()")
    
    # 2. 初始化Paddle Lite优化器
    opt = Opt()
    opt.set_model_file(model_file)
    opt.set_param_file(param_file)
    
    # 3. 设置移动端适配参数(ARM架构,手机通用)
    opt.set_valid_places("arm")
    opt.set_model_type("naive_buffer")  # 移动端轻量化格式
    
    # 4. 设置输出路径并执行优化
    opt.set_optimize_out(MOBILE_MODEL_PATH)
    opt.run()
    
    print(f"\n✅ 移动端模型已生成:{MOBILE_MODEL_PATH}.nb")
    print("   该文件可直接用于Android/iOS端部署")

# 主执行逻辑
if __name__ == "__main__":
    # 动态图→静态图→移动端模型
    dynamic2static()
    static2mobile()

image

使用paddle.lite执行端上模型预测:

package com.baidu.paddle.lite;

import android.content.Context;
import android.util.Log;

import com.baidu.paddle.lite.MobileConfig;
import com.baidu.paddle.lite.PaddlePredictor;
import com.baidu.paddle.lite.Tensor;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.FloatBuffer;

/**
 * Paddle Lite 模型管理类
 * 负责模型加载、预测、资源释放
 */
public class PaddleLiteManager {

    private static final String TAG = "PaddleLiteManager";
    // 模型文件名(请确保与assets中的文件完全一致,包括.nb后缀)
    private static final String MODEL_FILE_NAME = "ernie_mobile_model.nb";
    // 输入张量名称(根据你的模型实际输入名修改,可通过Paddle Lite工具查看)
    private static final String INPUT_TENSOR_NAME = "input";
    // 输出张量名称(根据你的模型实际输出名修改)
    private static final String OUTPUT_TENSOR_NAME = "output";

    private Context mContext;
    private PaddlePredictor mPredictor; // 预测器实例
    private boolean isInitSuccess = false; // 初始化状态

    public PaddleLiteManager(Context context) {
        this.mContext = context.getApplicationContext();
    }

    /**
     * 初始化预测器(核心:解决模型文件找不到的问题)
     */
    public boolean initPredictor() {
        try {
            // 1. 将assets中的模型文件复制到应用内部存储(避免assets路径访问限制)
            File modelFile = copyAssetFileToInternalStorage(MODEL_FILE_NAME);
            if (modelFile == null || !modelFile.exists()) {
                Log.e(TAG, "模型文件复制失败或不存在:" + (modelFile != null ? modelFile.getAbsolutePath() : "null"));
                return false;
            }
            Log.d(TAG, "模型文件路径:" + modelFile.getAbsolutePath());

            // 2. 配置MobileConfig
            MobileConfig config = new MobileConfig();
            config.setModelFromFile(modelFile.getAbsolutePath()); // 使用绝对路径加载
            config.setThreads(4); // 设置线程数(根据设备调整)
//            config.setPowerMode(MobileConfig.PowerMode.LITE_POWER_HIGH); // 高性能模式
            // 可选:设置精度模式(根据需求选择)
            // config.setPrecisionMode(MobileConfig.PrecisionMode.LITE_PRECISION_FP32);

            // 3. 创建预测器
            mPredictor = PaddlePredictor.createPaddlePredictor(config);
            if (mPredictor == null) {
                Log.e(TAG, "预测器创建失败");
                return false;
            }

            isInitSuccess = true;
            Log.d(TAG, "模型初始化成功!");
            return true;

        } catch (Exception e) {
            Log.e(TAG, "初始化预测器异常:", e);
            isInitSuccess = false;
            return false;
        }
    }

    /**
     * 执行预测(示例:输入float数组,返回输出结果)
     * @param inputData 输入数据(需与模型输入形状匹配,示例:[1, 128]的float数组)
     * @param inputShape 输入形状(示例:new long[]{1, 128})
     * @return 输出结果float数组,失败返回null
     */
    public float[] runPredict(float[] inputData, long[] inputShape) {
        if (!isInitSuccess || mPredictor == null) {
            Log.e(TAG, "预测器未初始化成功,无法执行预测");
            return null;
        }

        try {
            // 1. 获取输入张量
            Tensor inputTensor = mPredictor.getInput(0);
            if (inputTensor == null) {
                Log.e(TAG, "获取输入张量失败,张量名:" + INPUT_TENSOR_NAME);
                return null;
            }

            // 2. 设置输入形状和数据
            inputTensor.resize(inputShape);
            inputTensor.setData(inputData);

            // 3. 执行预测
            long startTime = System.currentTimeMillis();
            boolean predictResult = mPredictor.run();
            long endTime = System.currentTimeMillis();
            Log.d(TAG, "预测耗时:" + (endTime - startTime) + "ms");

            if (!predictResult) {
                Log.e(TAG, "预测执行失败");
                return null;
            }

            // 4. 获取输出张量
            Tensor outputTensor = mPredictor.getOutput(0);
            if (outputTensor == null) {
                Log.e(TAG, "获取输出张量失败,张量名:" + OUTPUT_TENSOR_NAME);
                return null;
            }

            // 5. 读取输出数据
//            float[] outputBuffer = outputTensor.getByteData();
//            float[] outputData = new float[outputBuffer.remaining()];
//            outputBuffer.get(outputData);

            Log.d(TAG, "预测成功,输出数据长度:" + outputTensor.getFloatData().length);
            return outputTensor.getFloatData();

        } catch (Exception e) {
            Log.e(TAG, "预测过程异常:", e);
            return null;
        }
    }

    /**
     * 释放资源
     */
    public void release() {
        if (mPredictor != null) {
//            mPredictor.close();
            mPredictor = null;
        }
        isInitSuccess = false;
        Log.d(TAG, "预测器资源已释放");
    }

    /**
     * 将assets中的文件复制到应用内部存储
     * @param fileName assets中的文件名
     * @return 复制后的文件,失败返回null
     */
    private File copyAssetFileToInternalStorage(String fileName) {
        InputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            // 目标文件路径:/data/data/com.baidu.paddle.lite/files/xxx.nb
            File destFile = new File(mContext.getFilesDir(), fileName);

            // 如果文件已存在,直接返回
            if (destFile.exists()) {
                Log.d(TAG, "模型文件已存在,无需重复复制:" + destFile.getAbsolutePath());
                return destFile;
            }

            // 从assets读取文件
            inputStream = mContext.getAssets().open(fileName);
            outputStream = new FileOutputStream(destFile);

            // 复制文件
            byte[] buffer = new byte[1024 * 4]; // 4KB缓冲区
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }

            outputStream.flush();
            Log.d(TAG, "模型文件复制成功:" + destFile.getAbsolutePath());
            return destFile;

        } catch (Exception e) {
            Log.e(TAG, "复制assets文件失败:", e);
            return null;
        } finally {
            // 关闭流
            try {
                if (inputStream != null) inputStream.close();
                if (outputStream != null) outputStream.close();
            } catch (Exception e) {
                Log.e(TAG, "关闭流异常:", e);
            }
        }
    }

    /**
     * 获取初始化状态
     */
    public boolean isInitSuccess() {
        return isInitSuccess;
    }
}

至此,我们就跑完了从端模型的初步探索:跑通了 Hadoop → Spark → 大模型轻量化 → 端侧部署 全流程Demo

posted @ 2026-02-12 13:33  灰色飘零  阅读(16)  评论(0)    收藏  举报