端侧大模型实践 - 生成预测模型&模型轻量化&端侧部署
为避免模型训练中出现内存异常,先临时增加交换内存:# 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
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 =
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()
batch_count += 1
print(f"Batch {batch_count} 训练完成,损失:{loss.numpy():.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("提示:权重警告是正常现象,模型已成功训练并保存!")训练记录如下:
验证模型训练结果,创建 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(], dtype="int64")
token_type_ids = paddle.to_tensor(], dtype="int64")
# 模型推理(禁用梯度计算,节省内存)
with paddle.no_grad():
logits = model(input_ids, token_type_ids)
# 获取概率最大的标签
pred_label = paddle.argmax(logits, axis=1).numpy()
# 返回直观结果
return label_map
# 4. 验证新文本(选3条未参与训练的文本)
test_texts = [
"快递到哪了?", # 预期:物流咨询
"退款多久能到账?", # 预期:退款咨询
"产品怎么使用?" # 预期:产品咨询
]
# 5. 执行验证并打印结果
print("==== 模型验证结果 ====")
for text in test_texts:
result = predict(text)
print(f"输入文本:{text}")
print(f"模型分类结果:{result}\n"):验证有问题,但至少跑通了(不过虽然结果不对,这里也先不做追究,因为是要跑通流程,针对细节暂时不关注)
因为默认训练后的模型是动态图模型,需要输出静态图模型和移动端模型:
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=, dtype="int64", name="input_ids"),
paddle.static.InputSpec(shape=, 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()
使用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 输入数据(需与模型输入形状匹配,示例:的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.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; // 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
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! 分享、互助 让互联网精神温暖你我 东西不错很实用谢谢分享 东西不错很实用谢谢分享 yyds。多谢分享 感谢分享 不错,里面软件多更新就更好了
页:
[1]