为避免模型训练中出现内存异常,先临时增加交换内存:- # 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("提示:权重警告是正常现象,模型已成功训练并保存!")
复制代码 训练记录如下:
验证模型训练结果,创建 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"):
复制代码 验证有问题,但至少跑通了(不过虽然结果不对,这里也先不做追究,因为是要跑通流程,针对细节暂时不关注)
因为默认训练后的模型是动态图模型,需要输出静态图模型和移动端模型:- 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()
复制代码
使用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
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |