Compare commits

...

3 Commits

Author SHA1 Message Date
1Dreamer666 0fee6a001a
Merge 743420daac into 512eed0b65 2026-07-08 18:37:49 +00:00
jingyaogong 512eed0b65 [fix] robustness 2026-06-28 18:03:34 +08:00
Toward 743420daac [fix] 更新.gitignore文件,添加忽略目录;修改web_demo.py,优化样式和功能 2026-05-10 23:09:26 +08:00
7 changed files with 109 additions and 36 deletions

8
.gitignore vendored
View File

@ -3,4 +3,10 @@ __pycache__/
.DS_Store
out
website/
docs-minimind/
docs-minimind/
checkpoints/
dataset/
minimind-3/
minimind-3-moe/

View File

@ -765,16 +765,16 @@ LoRA 是一种常见的参数高效微调(Parameter-Efficient Fine-Tuning, PEF
```bash
# train_lora.py 在 CPU 上通常也能比较轻快地完成
# 方式1
torchrun --nproc_per_node 1 train_lora.py
cd trainer && torchrun --nproc_per_node 1 train_lora.py
# 方式2
python train_lora.py
cd trainer && python train_lora.py
```
> 训练后的模型权重文件默认每隔`save_interval步`保存为: `lora_xxx_*.pth`(*为模型具体dimension,每次保存时新文件会覆盖旧文件)
LoRA 很适合处理“如何在尽量保留通用能力的前提下,让模型快速适应私有领域或垂直场景”这类问题。例如基础模型医学知识不足时,就可以在原有模型之上叠加一层面向医疗场景的 LoRA 权重,以较小代价获得更好的领域表现。
通常只需要准备同样的多轮对话格式数据,放置到 `lora_xxx.jsonl`,再执行 `python train_lora.py`,即可得到新的 `LoRA` 模型权重。
通常只需要准备同样的多轮对话格式数据,放置到 `lora_xxx.jsonl`,再从仓库根目录执行 `cd trainer && python train_lora.py`,即可得到新的 `LoRA` 模型权重。
例1:垂域数据

View File

@ -765,16 +765,16 @@ Its core idea is to introduce low-rank incremental branches alongside the origin
```bash
# train_lora.py can usually be completed fairly quickly even on CPU
# Method 1
torchrun --nproc_per_node 1 train_lora.py
cd trainer && torchrun --nproc_per_node 1 train_lora.py
# Method 2
python train_lora.py
cd trainer && python train_lora.py
```
> The trained model weight files are saved by default every `save_interval steps` as: `lora_xxx_*.pth` (* is the specific model dimension, each save overwrites the previous file)
LoRA is well-suited for handling problems like "how to let the model quickly adapt to private domains or vertical scenarios while preserving general capabilities as much as possible." For example, when the base model lacks medical knowledge, a medical-oriented LoRA weight layer can be stacked on top of the original model to achieve better domain performance at relatively small cost.
Usually you only need to prepare multi-turn dialogue format data in the same way, place it in `lora_xxx.jsonl`, and then run `python train_lora.py` to obtain new `LoRA` model weights.
Usually you only need to prepare multi-turn dialogue format data in the same way, place it in `lora_xxx.jsonl`, and then run `cd trainer && python train_lora.py` from the repository root to obtain new `LoRA` model weights.
Example 1: Vertical domain data
@ -1989,4 +1989,4 @@ If `MiniMind` has been helpful to your research or work, feel free to cite:
# ⚖️ License
This project is open-sourced under the [Apache License 2.0](LICENSE).
This project is open-sourced under the [Apache License 2.0](LICENSE).

5
scripts/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# .gitignore 忽略下列文件和目录
#比如minimind\scripts\minimind-3和minimind\scripts\minimind-3-moe
minimind-3/
minimind-3-moe/

View File

@ -21,13 +21,19 @@ while True:
extra_body={"chat_template_kwargs": {"open_thinking": True}, "reasoning_effort": "medium"} # 思考开关
)
if not stream:
if not response.choices or response.choices[0].message is None:
raise ValueError("LLM returned empty or filtered response")
assistant_res = response.choices[0].message.content
print('[A]: ', assistant_res)
else:
print('[A]: ', end='', flush=True)
assistant_res = ''
for chunk in response:
if not chunk.choices:
continue
delta = chunk.choices[0].delta
if delta is None:
continue
r = getattr(delta, 'reasoning_content', None) or ""
c = delta.content or ""
if r:
@ -37,4 +43,4 @@ while True:
assistant_res += c
conversation_history.append({"role": "assistant", "content": assistant_res})
print('\n\n')
print('\n\n')

View File

@ -15,7 +15,7 @@ from threading import Thread
from queue import Queue
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from pydantic import BaseModel, Field
from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
from model.model_lora import apply_lora, load_lora
@ -54,7 +54,7 @@ class ChatRequest(BaseModel):
top_p: float = 0.92
max_tokens: int = 8192
stream: bool = True
tools: list = []
tools: list = Field(default_factory=list)
open_thinking: bool = False
chat_template_kwargs: dict = None
@ -104,24 +104,28 @@ def parse_response(text):
def generate_stream_response(messages, temperature, top_p, max_tokens, tools=None, open_thinking=False):
try:
new_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools or None, open_thinking=open_thinking)[-max_tokens:]
new_prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, tools=tools or None, open_thinking=open_thinking)
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
queue = Queue()
streamer = CustomStreamer(tokenizer, queue)
def _generate():
model.generate(
inputs.input_ids,
max_new_tokens=max_tokens,
do_sample=True,
temperature=temperature,
top_p=top_p,
attention_mask=inputs.attention_mask,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
streamer=streamer
)
try:
model.generate(
inputs.input_ids,
max_new_tokens=max_tokens,
do_sample=True,
temperature=temperature,
top_p=top_p,
attention_mask=inputs.attention_mask,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
streamer=streamer
)
except Exception as e:
queue.put({"error": str(e)})
queue.put(None)
Thread(target=_generate).start()
@ -133,6 +137,9 @@ def generate_stream_response(messages, temperature, top_p, max_tokens, tools=Non
text = queue.get()
if text is None:
break
if isinstance(text, dict):
yield json.dumps(text, ensure_ascii=False)
continue
full_text += text
if not thinking_ended:
@ -190,7 +197,7 @@ async def chat_completions(request: ChatRequest):
add_generation_prompt=True,
tools=request.tools or None,
open_thinking=request.get_open_thinking()
)[-request.max_tokens:]
)
inputs = tokenizer(new_prompt, return_tensors="pt", truncation=True).to(device)
with torch.no_grad():
generated_ids = model.generate(

View File

@ -63,6 +63,28 @@ st.markdown("""
transition: all 0.2s ease !important;
margin: 0 2px !important; /* 调整这里的 margin 值 */
}
/* 单独恢复侧边栏按钮(新建对话)的正常横向长方形外观,避免文字被挤成竖排 */
[data-testid="stSidebar"] .stButton > button {
border-radius: 8px !important; /* 恢复稍微圆角的矩形 */
width: 100% !important; /* 撑满侧边栏宽度 */
height: auto !important;
min-width: 100% !important; /* 覆盖掉 18px 的限制 */
min-height: 40px !important;
max-width: 100% !important;
max-height: none !important;
padding: 8px !important; /* 恢复内边距 */
background-color: #f4f6f8 !important; /* 增加一点按钮底色以免透明看不清 */
border: 1px solid #e0e0e0 !important;
font-size: 15px !important;
color: #333 !important;
margin: 10px 0 !important;
}
[data-testid="stSidebar"] .stButton > button:hover {
border-color: #999 !important;
background-color: #e4e8f0 !important;
color: #000 !important;
}
</style>
""", unsafe_allow_html=True)
@ -83,6 +105,7 @@ LANG_TEXTS = {
'disclaimer': 'AI 生成内容可能存在错误,请仔细核实',
'think_tip': '自适应思考,目前多轮对话或Tool Call共存时思考不稳定',
'tool_select': '工具选择(最多4个)',
'new_chat': '新建对话', # 新增
},
'en': {
'settings': 'Model Settings',
@ -96,6 +119,7 @@ LANG_TEXTS = {
'disclaimer': 'AI-generated content may be inaccurate, please verify',
'think_tip': 'Adaptive thinking; may be unstable with multi-turn or Tool Call',
'tool_select': 'Tool Selection (max 4)',
'new_chat': 'New Chat', # 新增
}
}
@ -237,15 +261,20 @@ def regenerate_answer(index):
# 动态扫描模型目录
script_dir = os.path.dirname(os.path.abspath(__file__))
MODEL_PATHS = {}
for d in sorted(os.listdir(script_dir), reverse=True):
full_path = os.path.join(script_dir, d)
if os.path.isdir(full_path) and not d.startswith('.') and not d.startswith('_'):
if any(f.endswith(('.bin', '.safetensors', '.pt')) or os.path.exists(os.path.join(full_path, 'model.safetensors.index.json')) for f in os.listdir(full_path) if os.path.isfile(os.path.join(full_path, f))):
MODEL_PATHS[d] = [d, d]
if not MODEL_PATHS:
MODEL_PATHS = {"No models found": ["", "No models"]}
@st.cache_data(ttl=3600) # 增加缓存(有效期1小时),避免每次刷新页面都去高频读取硬盘文件树
def get_model_paths():
script_dir = os.path.dirname(os.path.abspath(__file__))
paths = {}
for d in sorted(os.listdir(script_dir), reverse=True):
full_path = os.path.join(script_dir, d)
if os.path.isdir(full_path) and not d.startswith('.') and not d.startswith('_'):
if any(f.endswith(('.bin', '.safetensors', '.pt')) or os.path.exists(os.path.join(full_path, 'model.safetensors.index.json')) for f in os.listdir(full_path) if os.path.isfile(os.path.join(full_path, f))):
paths[d] =[d, d]
if not paths:
paths = {"No models found": ["", "No models"]}
return paths
MODEL_PATHS = get_model_paths()
# 模型选择
selected_model = st.sidebar.selectbox('Model', list(MODEL_PATHS.keys()), index=0)
@ -285,7 +314,27 @@ with st.sidebar.expander(get_text('tools')):
if checked and len(st.session_state.selected_tools) < 4:
st.session_state.selected_tools.append(name)
image_url = "https://www.modelscope.cn/api/v1/studio/gongjy/MiniMind/repo?Revision=master&FilePath=images%2Flogo2.png&View=true"
# 新建对话
st.sidebar.markdown('<hr style="margin: 12px 0 16px 0;">', unsafe_allow_html=True)
# 新建对话
if st.sidebar.button(get_text('new_chat'), key="new_chat", use_container_width=True):
clear_chat_messages()
st.rerun()
# 将外部网络图片改为读取本地图片转 base64 渲染
import base64
@st.cache_data
def get_image_base64(image_path):
try:
with open(image_path, "rb") as img_file:
return f"data:image/png;base64,{base64.b64encode(img_file.read()).decode()}"
except:
# 如果本地图片不存在,再退回使用网络URL
return "https://www.modelscope.cn/api/v1/studio/gongjy/MiniMind/repo?Revision=master&FilePath=images%2Flogo2.png&View=true"
local_logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "images", "logo2.png")
image_url = get_image_base64(local_logo_path)
st.markdown(
f'<div style="display: flex; flex-direction: column; align-items: center; text-align: center; margin: 0; padding: 0;">'
@ -339,8 +388,8 @@ def main():
st.markdown(
f'<div style="display: flex; justify-content: flex-end;"><div style="display: inline-block; margin: 10px 0; padding: 8px 12px 8px 12px; background-color: #3d4450; border-radius: 22px; color: white;">{prompt}</div></div>',
unsafe_allow_html=True)
messages.append({"role": "user", "content": prompt[-st.session_state.max_new_tokens:]})
st.session_state.chat_messages.append({"role": "user", "content": prompt[-st.session_state.max_new_tokens:]})
messages.append({"role": "user", "content": prompt})
st.session_state.chat_messages.append({"role": "user", "content": prompt})
placeholder = st.empty()