
简介这份PDF资料面向AI零基础、仅掌握JavaScript或略懂PHP、Python的初学者系统讲解如何在本地对DeepSeek-R1:1.5B模型进行微调训练。内容从Ollama本地部署、模型文件路径确认到训练数据准备、Python环境搭建、torch/transformers/datasets三大库安装再到加载预训练模型与分词器、数据预处理形成一条完整可跟随的实践链路。资源包共1个PDF文件大小约1.93MB以图文步骤形式呈现便于边看边操作。目前已有270人学习下载。读者可借此掌握本地模型微调的目录结构设计、代码编写与运行验证方法理解模型加载成功与数据预处理成功的判断依据并获得依赖库安装缓慢时的耐心等待与排错思路适合希望低门槛入门AI模型训练的实践者参考。1. 从 Ollama 拉下来的模型到底能不能接着训很多人第一次在本地跑通ollama run deepseek-r1:1.5b之后都会冒出一个念头这模型能不能拿我自己的数据再喂一喂尤其是手头只有一堆.txt语料、平时主要写 JavaScript、顶多翻过几页 PHP 和 Python 的从业者最关心的不是论文里的 scaling law而是「我这点数据能不能让它说话更像我的业务」。答案是能但路径和你想的不太一样。Ollama 负责的是推理侧的本地部署它把模型权重、量化格式、运行环境打包成一个开箱即用的黑匣子而微调是训练侧的事需要把权重从 Ollama 的模型目录里「请」出来交给 PyTorch 和 Hugging Face 那套工具链。这两件事共用同一份模型文件但操作入口完全不同。这篇笔记就按「部署 → 取权重 → 装环境 → 写脚本 → 训练 → 验证」的顺序拆一遍目标读者是只会 JavaScript、Python 几乎零基础、但愿意照着敲命令的人。全程在 Windows 上操作模型放在D:\ollama\models工作目录放在D:\ollama\fine_tune_deepseek不涉及任何云端算力纯本地跑。需要先打一个预防针deepseek-r1:1.5b这个体量在消费级显卡上做全量微调基本不现实显存会直接爆掉。常见做法是走 LoRA 或者只做小学习率的轻量微调甚至退一步只做数据格式对齐。这篇笔记会把每一步的边界讲清楚哪些参数能改、哪些改了会翻车、日志里出现什么字样说明该停手都会落到具体命令和代码上。2. 把 Ollama 模型目录变成 Hugging Face 能读的权重2.1 先搞清楚 Ollama 的模型文件长什么样Ollama 在 Windows 上默认把模型放在C:\Users\用户名\.ollama\models但安装时可以指定到别的盘。你如果按项目里的做法装到了D:\ollama\models那目录结构大致是这样D:\ollama\models\ ├── blobs\ │ ├── sha256-xxxxx │ └── sha256-yyyyy └── manifests\ └── registry.ollama.ai\ └── library\ └── deepseek-r1\ └── 1.5bblobs里是真正的权重分片和配置manifests里是索引文件记录了这个模型由哪些 blob 组成、用什么模板、量化类型是什么。这里有个关键点Ollama 拉下来的模型通常是 GGUF 或它自己封装的量化格式而transformers的AutoModelForCausalLM.from_pretrained()默认读的是 PyTorch 的pytorch_model.bin或model.safetensors。两者不是一回事。所以直接写model_name D:/ollama/models/deepseek-r1:1.5b然后调from_pretrained大概率会报OSError: Cant load config或者找不到权重文件。这不是路径写错了是格式不匹配。2.2 两条可行的取权重路线路线一从 Hugging Face 重新拉一份同规格的原始权重。deepseek-r1:1.5b对应的基座模型在 Hugging Face 上有公开仓库用transformers直接按模型名加载即可。这条路最省事缺点是下载量不小网络慢的时候要等很久。路线二用转换脚本把 Ollama 的 blob 转成 PyTorch 格式。Ollama 的 manifest 里记录了每个 blob 的 media type如果是application/vnd.ollama.image.model那它就是 GGUF 格式的权重。可以用llama.cpp自带的转换脚本先转成 GGUF 再转 PyTorch但这条链路对零基础的人不友好中间任何一步版本对不上都会卡住。我一般会建议走路线一因为微调本身就需要一个干净的、未量化的基座量化过的权重再训效果会打折扣。下面这段代码就是按路线一写的把模型名换成 Hugging Face 上的实际仓库名即可# load_base.py # 作用从 Hugging Face 拉取与 deepseek-r1:1.5b 同规格的基座权重 # 注意首次运行会下载数 GB 文件确保 D 盘有足够空间 from transformers import AutoTokenizer, AutoModelForCausalLM # 这里的模型名需要替换为 Hugging Face 上对应的仓库标识 # 不要直接写本地 Ollama 目录格式不兼容 model_name deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypeauto, # 自动选择 fp16 或 fp32省显存 device_mapauto # 有 GPU 就上 GPU没有就 CPU ) print(Model and tokenizer loaded successfully!) print(Model dtype:, model.dtype) print(Model device:, next(model.parameters()).device)逻辑说明torch_dtypeauto让 transformers 根据当前硬件自动选精度有 CUDA 且支持 fp16 时会用半精度显存占用直接减半。device_mapauto会把模型层自动分配到可用设备上单卡场景下就是全部放 GPU显存不够时会往 CPU 上溢但速度会掉得很厉害。参数说明如果你只有 CPU把device_map去掉加载完后手动model.to(cpu)。如果显存小于 6GB建议加load_in_8bitTrue但这需要额外装bitsandbytesWindows 上编译经常出问题新手先别碰。2.3 验证加载是否真的成功运行python load_base.py看到Model and tokenizer loaded successfully!只说明没抛异常不代表权重完整。再补一句检查# 检查模型参数量和第一层权重的形状 total_params sum(p.numel() for p in model.parameters()) print(fTotal parameters: {total_params / 1e6:.2f}M) print(First layer shape:, model.model.embed_tokens.weight.shape)1.5B 的模型参数量应该在 1500M 上下embedding 层的词表大小通常在 15 万左右。如果打印出来是 0M 或者形状明显不对说明加载的是空壳回去检查模型名和网络。3. 数据预处理把 .txt 喂成模型能吃的 token3.1 语料格式和目录约定项目里说每行可以是一个训练样本这个说法在纯文本继续预训练场景下成立但要注意两点一是每行不要太短低于 10 个字符的样本对模型几乎没有信息量二是编码必须是 UTF-8Windows 记事本默认可能是 GBK读进来会乱码。目录按项目里的约定放D:\ollama\fine_tune_deepseek\ ├── data\ │ ├── file1.txt │ ├── file2.txt │ └── file3.txt └── fine_tune.py3.2 读取和 tokenize 的完整脚本# data_prep.py # 作用读取 data 目录下所有 .txt转成模型输入 import os from transformers import AutoTokenizer data_dir D:/ollama/fine_tune_deepseek/data model_name deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B tokenizer AutoTokenizer.from_pretrained(model_name) # 如果 tokenizer 没有 pad_token用 eos_token 顶上 # 否则 paddingmax_length 会直接报错 if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token texts [] for filename in os.listdir(data_dir): if filename.endswith(.txt): filepath os.path.join(data_dir, filename) with open(filepath, r, encodingutf-8) as f: for line in f: line line.strip() if len(line) 10: # 过滤过短样本 texts.append(line) print(fLoaded {len(texts)} samples) inputs tokenizer( texts, return_tensorspt, max_length512, truncationTrue, paddingmax_length ) print(Input shape:, inputs[input_ids].shape) print(Data preprocessed successfully!)逻辑说明os.listdir遍历目录endswith(.txt)只挑文本文件。逐行读取而不是f.read()整篇读入是为了让每个样本独立避免一篇长文被截断后语义不完整。len(line) 10这个阈值可以按你的语料调整目的是过滤掉「你好」「好的」这类无信息样本。参数说明max_length512是单样本最大 token 数1.5B 模型通常支持 2048 甚至更长但微调时设太大显存吃不消512 是消费级显卡的稳妥值。truncationTrue表示超长截断paddingmax_length表示短样本补齐到 512这样 batch 里所有样本长度一致训练时不会因为动态长度反复重编译。3.3 一个容易被忽略的坑labels 从哪来上面这段代码只产出了input_ids和attention_mask没有labels。因果语言模型的训练目标是预测下一个 tokenlabels通常就是input_ids本身由 Trainer 内部做 shift。但如果你直接把inputs[input_ids]当train_dataset传进去Trainer 会把它当成一个普通张量取不到labels字段训练时 loss 会是 None。正确做法是构造一个 Dataset 对象把input_ids和attention_mask都放进去并让labels等于input_idsfrom datasets import Dataset dataset Dataset.from_dict({ input_ids: inputs[input_ids], attention_mask: inputs[attention_mask], labels: inputs[input_ids].clone() # 因果 LM 的标签就是输入本身 }) print(dataset)这一步不做后面trainer.train()会直接抛ValueError: Trainer: No labels found很多人卡在这里以为是数据问题其实是结构没对上。4. 微调参数怎么设Trainer 和 TrainingArguments 的实操配置4.1 最小可运行的训练脚本# train.py # 作用用 Trainer API 对基座模型做轻量微调 import os from transformers import ( AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments ) from datasets import Dataset model_name deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B data_dir D:/ollama/fine_tune_deepseek/data output_dir D:/ollama/fine_tune_deepseek/fine_tuned_model log_dir D:/ollama/fine_tune_deepseek/logs tokenizer AutoTokenizer.from_pretrained(model_name) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypeauto, device_mapauto ) # 读取数据 texts [] for filename in os.listdir(data_dir): if filename.endswith(.txt): with open(os.path.join(data_dir, filename), r, encodingutf-8) as f: for line in f: line line.strip() if len(line) 10: texts.append(line) inputs tokenizer( texts, return_tensorspt, max_length512, truncationTrue, paddingmax_length ) dataset Dataset.from_dict({ input_ids: inputs[input_ids], attention_mask: inputs[attention_mask], labels: inputs[input_ids].clone() }) # 训练参数 training_args TrainingArguments( output_diroutput_dir, logging_dirlog_dir, num_train_epochs3, # 小数据集 3 轮足够多了会过拟合 per_device_train_batch_size1, # 显存小于 8GB 就保持 1 gradient_accumulation_steps8, # 等效 batch size 1 * 8 8 learning_rate2e-5, # 微调常用量级别超过 5e-5 warmup_steps20, logging_steps10, save_steps100, save_total_limit2, # 只保留最近两个 checkpoint省磁盘 fp16True, # 有 NVIDIA GPU 就开AMD 或 CPU 关掉 report_totensorboard ) trainer Trainer( modelmodel, argstraining_args, train_datasetdataset ) trainer.train() trainer.save_model(output_dir) tokenizer.save_pretrained(output_dir) print(fFine-tuned model saved to {output_dir})逻辑说明gradient_accumulation_steps8是在显存不够时模拟大 batch 的标准手段梯度会累积 8 次再更新一次参数等效 batch size 变成 8训练稳定性比单样本好很多。save_total_limit2防止 checkpoint 把 D 盘塞满1.5B 模型每个 checkpoint 大约 3GB不限制的话跑几轮就几十 GB 出去了。参数说明learning_rate2e-5是微调的甜点区再大容易把预训练学到的知识冲掉再小则几乎看不出变化。num_train_epochs3针对几千条样本的规模如果你的.txt只有几百行跑 1 轮就够跑多了模型会把你的语料背下来生成时只会复读。4.2 用 TensorBoard 看 loss 曲线训练启动后另开一个终端cd D:\ollama\fine_tune_deepseek tensorboard --logdir./logs浏览器打开http://localhost:6006重点看train/loss这条线。正常情况是从 2.0 左右缓慢下降到 1.0 以下如果前 50 步就掉到 0.1说明学习率太大或者数据重复度太高如果 200 步还在 2.5 以上横盘检查labels是不是没对上或者数据里全是乱码。4.3 显存不够时的降级方案如果fp16True加上 batch size 1 还是 OOM按这个顺序降调整项改法代价序列长度max_length从 512 降到 256长样本被截断语义可能不完整精度fp16改bf16需 GPU 支持老卡不支持会报错量化加载加load_in_8bitTrue需装 bitsandbytesWindows 编译易失败模型层冻结只训最后 4 层效果打折但显存立省一半冻结层的写法是在Trainer之前加# 只训练最后 4 层其余层冻结 for name, param in model.named_parameters(): if layers in name: layer_idx int(name.split(layers.)[1].split(.)[0]) if layer_idx 24: # 假设总共 28 层冻结前 24 层 param.requires_grad False层数需要根据实际模型结构打印model确认不同版本的层命名可能不一样。5. 避坑排查训练过程中最常见的五类翻车5.1 现象from_pretrained报OSError: Cant load config for D:/ollama/models/deepseek-r1:1.5b原因Ollama 的模型目录里没有config.json只有 manifest 和 blobtransformers 不认识这种结构。解决不要指向 Ollama 目录改用 Hugging Face 上的同规格仓库名或者先用转换脚本把 blob 转成标准格式。转换脚本涉及 GGUF 解析零基础不建议自己写直接用公开权重最稳。5.2 现象训练时 loss 一直是nan原因学习率太大或者数据里有空样本导致 attention mask 全零softmax 时出现除零。解决先把learning_rate降到5e-6试一轮同时在数据读取阶段加一行if len(line) 0: continue确保没有空行进入 tokenizer。如果还不行检查fp16是否在你的显卡上不稳定换成bf16或直接关掉。5.3 现象pip install torch卡在下载进度条几小时不动原因默认源在国外带宽受限。项目正文里提到 22.2 KB/s 的速度204MB 要下两个多小时这是常态。解决换国内镜像源命令改成pip install torch transformers datasets -i https://pypi.tuna.tsinghua.edu.cn/simple如果 torch 的 CUDA 版本需要匹配显卡驱动先去 PyTorch 官网查对应命令再在命令末尾加-i镜像参数。注意镜像源不一定同步所有版本如果报No matching distribution去掉镜像用默认源重试。5.4 现象训练跑完了但生成结果和微调前一模一样原因学习率太小、训练轮数太少或者数据量本身不足以让权重发生可观测的偏移。1.5B 模型在几百条样本上微调变化本来就微弱。解决先把num_train_epochs提到 5learning_rate提到5e-5跑一轮看 loss 有没有明显下降。如果 loss 降了但生成没变说明模型把你的数据「记住」了但没有泛化这时候要检查数据质量而不是继续加轮数。5.5 现象保存后的模型用 Ollama 加载不了原因save_pretrained存的是 PyTorch 格式Ollama 只认 GGUF 或它自己的封装格式。解决微调完的模型要回到 Ollama 用需要先转 GGUF再写 Modelfile 导入。转换用llama.cpp的convert_hf_to_gguf.py导入用ollama create。这条链路是另一个话题但心里要有数微调产物和 Ollama 推理产物之间隔着一层格式转换不是存完就能直接ollama run。6. 验证微调效果三个比「看起来像」更靠谱的检查手段训练跑完最忌讳的就是看一眼生成结果觉得「好像还行」就收工。下面三个检查按顺序做能过滤掉大部分自欺欺人的情况。第一个检查是困惑度对比。在留出的验证集上分别算基座模型和微调模型的 perplexity如果微调后反而升高说明过拟合了回去减轮数。代码不复杂import torch from transformers import AutoTokenizer, AutoModelForCausalLM def compute_ppl(model_path, texts): tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForCausalLM.from_pretrained(model_path, torch_dtypeauto, device_mapauto) model.eval() total_loss 0 with torch.no_grad(): for text in texts: inputs tokenizer(text, return_tensorspt).to(model.device) outputs model(**inputs, labelsinputs[input_ids]) total_loss outputs.loss.item() return torch.exp(torch.tensor(total_loss / len(texts))).item() # 准备 20 条没参与训练的文本做验证 val_texts [验证文本1, 验证文本2] # 替换成你的实际留出数据 print(Base PPL:, compute_ppl(deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B, val_texts)) print(Tuned PPL:, compute_ppl(D:/ollama/fine_tune_deepseek/fine_tuned_model, val_texts))第二个检查是固定 prompt 的生成对比。选 5 个你业务里真实会问的问题分别用基座和微调模型生成把输出并排贴出来看。重点不是「哪个更好」而是「微调后的输出有没有带上你语料的风格特征」。如果风格没变说明数据没有真正影响模型。第三个检查是灾难性遗忘测试。用几个通用问题比如「解释一下什么是递归」问微调后的模型如果它连通用问题都答得乱七八糟说明学习率太大把预训练知识冲掉了。这时候要么降学习率重训要么在训练数据里混入一部分通用语料做回放。我自己的习惯是每次微调完先跑这三个检查任何一个不过关就不往 Ollama 里导。格式转换和导入那一步很费时间与其导进去发现不对再回滚不如在 PyTorch 阶段就把问题拦住。从那以后我每次动学习率和轮数之前都会先把验证集 perplexity 的基线记下来改完参数第一件事就是对比这个数比看生成文本靠谱得多。希望帮到你。本文还有配套的精品资源点击获取