簡述
MiniMind-V 是一個超適合初學者的項目,讓你用普通電腦就能訓一個能看圖說話的 AI。訓練過程就像教小孩:先準備好圖文材料(數據集),教它基礎知識(預訓練),再教具體技能(微調),最后測試它學會沒。整個過程簡單、便宜、快,1小時就能搞定,特別適合想玩 AI 又沒大預算的人!
GitHub - jingyaogong/minimind-v: 🚀 「大模型」1小時從0訓練26M參數的視覺多模態VLM!🌏 Train a 26M-parameter VLM from scratch in just 1 hours!🚀 「大模型」1小時從0訓練26M參數的視覺多模態VLM!🌏 Train a 26M-parameter VLM from scratch in just 1 hours! - jingyaogong/minimind-vhttps://github.com/jingyaogong/minimind-v
MiniMind-V 是什么?
MiniMind-V 是一個開源項目,目標是讓你從零開始訓練一個視覺語言模型(VLM)。簡單來說,VLM 是一種能同時理解文字和圖片的 AI。比如,你給它一張狗的照片,問“這是啥?”,它能回答“這是只狗!”
這個項目的亮點是:
- 超輕量:只有2600萬個參數(相比像 GPT-3 那樣的模型有幾十億參數,簡直是小巫見大巫)。
- 訓練快:在一塊不錯的個人顯卡(比如 NVIDIA 3090)上,1小時就能訓好。
- 成本低:租個 GPU 服務器訓練,費用大概1.3塊錢(約0.2美元)。
- 適合新手:代碼簡單,硬件要求不高,普通電腦也能跑。
就像是打造一個能看圖說話的小 AI 腦子,不需要超級計算機!想象 VLM 是一個有兩種超能力的 AI:
- 文字能力:能像聊天機器人(比如 ChatGPT)一樣理解和生成文字。
- 圖像能力:能“看”圖片,識別里面的內容。
MiniMind-V 是基于一個叫 MiniMind 的文字模型,加上一個視覺編碼器(把圖片變成 AI 能懂的數據),讓它能同時處理文字和圖片。簡單來說,MiniMind-V 接收文字和圖片,處理后生成回答。流程是這樣的:
- 輸入:你給它文字和圖片。比如,問“圖片里是什么?”并附上一張圖。圖片在代碼里用特殊符號(像“@@@…@@@”)表示。
- 處理圖片:圖片被送進一個叫 CLIP-ViT-Base-Patch16 的視覺編碼器。它把圖片切成小塊(像拼圖),轉化成一串數字(叫“令牌”),AI 就能理解了。
- 處理文字:你的問題(比如“圖片里是什么?”)也被轉成令牌。
- 融合圖文:圖片令牌通過簡單的數學運算(線性變換)調整到跟文字令牌“說同一種語言”。這樣 AI 就能同時處理兩者。
- 輸出:這些令牌被送進語言模型,生成回答,比如“圖片里是一只狗在公園玩”。
這讓 MiniMind-V 能描述圖片、回答關于圖片的問題,甚至同時聊多張圖。
MiniMind-V的結構僅增加Visual Encoder和特征投影兩個子模塊,增加模態混合分支,以支持多種模態信息的輸入,引用官方給的圖來說明
在minimind-v
中,使用196個字符組成的?@@@...@@@
?占位符代替圖像,之所以是196個字符,前面有所提及: 任何圖像都被clip模型encoder為196×768維的token, 因此minimind-v
的prompt為:
@@@......@@@\n這個圖片描述的是什么內容?
計算完embedding和projection,并對圖像部分token替換后整個計算過程到輸出則和LLM部分沒有任何區別。
一次性多圖的實現方法就是通過注入多個<image>
圖像占位符進行實現,不需要修改任何框架
MiniMind-V 怎么訓練?
訓練 VLM 就像教小孩學會看圖說話。你得給它看很多例子(數據),慢慢引導它學會規律。以下是訓練步驟,用最簡單的語言解釋:
1. 準備材料(數據和工具)
- 數據集:需要一堆圖片和對應的文字說明或問題。比如,一張貓的圖片配上“這是一只貓”或者“這是什么動物?”。MiniMind-V 的數據集(約5GB)包括圖片和文字文件(JSONL 格式),放在 ./dataset 文件夾。你得下載并解壓。
- 預訓練語言模型:MiniMind-V 用一個叫 MiniMind 的文字模型作為基礎。這個模型已經會聊天了,相當于給 AI 一個起點。你下載它的權重(預訓練數據),放進 ./out 文件夾。
- 視覺編碼器:用一個叫 CLIP 的預訓練模型(clip-vit-base-patch16)來處理圖片。CLIP 已經很會看圖了,不用從頭訓。你下載它到 ./model/vision_model 文件夾。
- 軟件環境:安裝代碼需要的 Python 庫(列在 requirements.txt 里),就像做飯前把廚房工具準備好。
2. 預訓練(學基礎知識)
- 干啥:預訓練是教 AI 怎么把圖片和文字“連起來”。你給它看一大堆圖文對(比如一棵樹的圖片配“這是一棵樹”),讓它學會圖片和文字的關聯。
- 咋做:運行代碼 1-pretrain_vlm.py,把數據集喂給模型。CLIP 把圖片轉成令牌(每張圖生成196個令牌,每個令牌是768個數字)。這些令牌跟文字令牌混在一起,模型學著預測接下來是什么(比如看到狗的圖片和“這”之后,預測“是只狗”)。
- 時間和資源:用 NVIDIA 3090 顯卡,大概1小時搞定。因為模型小(2600萬參數),數據集也不大,跑得快。訓練完會生成一個文件(比如 vlm_pretrain.pth),保存學到的東西。
- 可跳過:如果硬盤空間不夠,你可以用項目提供的預訓練權重跳過這一步,但自己訓會讓模型更強。
3. 監督微調(SFT)(教具體技能)
- 干啥:微調是教 AI 怎么回答具體問題或按指令辦事。比如,你給它看一張日落圖,問“這是啥?”,它得學會回答“這是日落”。
- 咋做:運行代碼 train_sft_vlm.py,用一個包含問題-答案對(帶圖片)的數據集。模型反復練習生成正確答案,調整自己變得更聰明。就像考試前刷題一樣。
- 輸出:生成一個文件(比如 vlm_sft.pth),保存微調后的模型。這讓 AI 在實際任務(比如聊圖片)上表現更好。
- 為啥重要:微調讓模型更實用,能真正幫你解決問題,而不是只會背圖文對。
4. 測試模型(看看學得咋樣)
- 干啥:訓練完后,你可以用代碼 3-test_vlm.py 測試模型。給它一張圖和一個問題,看它回答得對不對。
- 例子:你上傳一張狗的圖片,問“這是啥?”,模型應該回答“這是只狗”。如果答錯了,說明訓練可能還得調調。
5 重點分析下
這個繼承自語言模型的視覺語言模型
class MiniMindVLM(MiniMindForCausalLM):config_class = VLMConfigdef __init__(self, params: VLMConfig = None, vision_model_path="./model/vision_model/clip-vit-base-patch16"):super().__init__(params)if not params: params = VLMConfig()self.params = paramsself.vision_encoder, self.processor = self.__class__.get_vision_model(vision_model_path)self.vision_proj = VisionProj(hidden_size=params.hidden_size)
模型的整體功能
MiniMindVLM 是一個視覺語言模型,它的核心任務是:
- 輸入:接收文本(以 input_ids 表示,類似分詞后的文字編號)和/或圖像(以 pixel_values 表示,類似像素數據)。
- 輸出:生成預測結果(logits,表示每個詞的概率分布),可以用來生成文本或回答問題。
- 額外功能:支持緩存(past_key_values)以加速生成,支持混合專家(MoE)機制(通過 aux_loss),并能處理視覺和文本的混合輸入。
從代碼看,模型的結構可以分為以下幾個主要部分:
- 嵌入層(embed_tokens):將輸入的文本 token 轉為向量表示。
- 視覺編碼器(vision_encoder):處理圖像輸入,生成圖像特征。
- 變換器層(model.layers):核心的神經網絡層,處理文本和圖像特征。
- 輸出頭(lm_head):將處理后的特征轉為預測結果。
- 輔助組件:如歸一化(norm)、位置編碼(freqs_cos 和 freqs_sin)和混合專家損失(aux_loss)。
forward方法
def forward(self,input_ids: Optional[torch.Tensor] = None,attention_mask: Optional[torch.Tensor] = None,past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,use_cache: bool = False,logits_to_keep: Union[int, torch.Tensor] = 0,pixel_values: Optional[torch.FloatTensor] = None,**args):
- input_ids:文本輸入的 token ID(形狀 [batch_size, seq_length]),表示分詞后的文本序列。例如,“你好”可能被編碼為 [101, 203, 102]。
- attention_mask:注意力掩碼,標記哪些 token 需要關注(1 表示關注,0 表示忽略),用于處理變長序列。
- past_key_values:緩存的鍵值對,用于加速自回歸生成(比如生成長文本時復用之前的計算結果)。
- use_cache:是否啟用緩存,True 時返回 past_key_values。
- logits_to_keep:指定輸出的 logits 范圍(整數或索引),控制返回哪些位置的預測結果。
- pixel_values:圖像輸入(形狀可能是 [batch_size, num_images, channels, height, width]),表示一批圖片的像素數據。
- args:其他未使用的參數,增加靈活性。
模型支持同時處理文本和圖像(pixel_values 可選),并且通過 past_key_values 和 use_cache 支持增量生成,說明這是一個自回歸變換器模型(類似 GPT),但增加了視覺處理能力。
其中圖像處理的方法
if pixel_values is not None and start_pos == 0:if len(pixel_values.shape) == 6:pixel_values = pixel_values.squeeze(2)bs, num, c, im_h, im_w = pixel_values.shapestack_dim = 1 if bs > 1 else 0vision_tensors = torch.stack([MiniMindVLM.get_image_embeddings(pixel_values[:, i, :, :, :], self.vision_encoder)for i in range(num局化num)], dim=stack_dim)hidden_states = self.count_vision_proj(tokens=input_ids, h=hidden_states, vision_tensors=vision_tensors,seqlen=input_ids.shape[1])
- 形狀調整:
- 如果 pixel_values 是 6 維(可能包含額外維度),用 squeeze(2) 移除第 2 維。
- 解析形狀為 [batch_size, num_images, channels, height, width],表示每批有 num_images 張圖片。
- 圖像編碼:
- 遍歷每張圖片(pixel_values[:, i, :, :, :]),用 self.vision_encoder(可能是 CLIP 模型)生成圖像嵌入(get_image_embeddings)。
- 將所有圖像嵌入堆疊為 vision_tensors(形狀取決于批次大小)。
- 融合圖像和文本:
- self.count_vision_proj:將圖像嵌入(vision_tensors)與文本嵌入(hidden_states)融合,生成新的 hidden_states。
- 融合考慮了 input_ids 和序列長度(seqlen),可能通過投影層(線性變換)將圖像特征映射到文本特征空間。
結構功能:
- vision_encoder 是一個獨立的視覺編碼器(這里使用的是CLIP-ViT-Base-Patch16),專門處理圖像,輸出固定維度的特征向量。
- count_vision_proj 是一個投影層,將圖像特征與文本特征對齊,表明模型在結構上將視覺和語言信息融合到一個統一的表示空間。
- 圖像處理只在序列開始(start_pos == 0)執行,說明圖像特征在生成后續 token 時會被緩存(通過 past_key_values)。
位置編碼
使用旋轉位置編碼(Rotary Position Embedding, RoPE)
position_embeddings = (self.model.freqs_cos[start_pos:start_pos + seq_length],self.model.freqs_sin[start_pos:start_pos + seq_length]
)
歸一化和混合專家損失
hidden_states = self.model.norm(hidden_states)aux_loss = sum(layer.mlp.aux_lossfor layer in self.model.layersif isinstance(layer.mlp, MOEFeedForward)
)
混合專家(MoE)損失:
- 檢查每層的 mlp(前饋網絡)是否是 MOEFeedForward 類型。
- 如果是,累加其 aux_loss(輔助損失,可能用于 MoE 的負載均衡)。
-
aux_loss 是 MoE 的正則化項,確保專家使用均衡,說明模型可能在性能和效率間做了優化。
6. 模型特點與設計亮點
- 視覺-語言融合:
- 通過 vision_encoder 和 count_vision_proj,模型將圖像和文本特征統一到同一表示空間,支持多模態任務(如圖像描述、視覺問答)。
- 圖像處理只在序列開始執行,優化了增量生成的效率。
- 高效變換器:
- 使用旋轉位置編碼(RoPE),減少位置編碼的內存開銷。
- 支持 MoE 機制(MOEFeedForward),通過稀疏激活降低計算量,適合輕量模型(MiniMind-V 只有 2600 萬參數)。
- 緩存機制(past_key_values)加速長序列生成。
- 靈活輸出:
- logits_to_keep 允許控制輸出范圍,優化計算或生成特定長度的序列。
- self.OUT 提供多種輸出(logits、hidden_states、past_key_values),支持多種下游任務。
- 輕量設計:
- 結合 Dropout 和 LayerNorm,模型訓練穩定,適合在消費級 GPU上運行。
- MiniMind-V 的小規模(2600 萬參數)和快速訓練(1 小時)表明它在結構上做了大量優化。
模型訓練
下載模型基座
下載clip模型到 ./model/vision_model 目錄下,CLIP-ViT-Base-Patch16 像一個“圖像翻譯器”,把圖片內容翻譯成語言模型能懂的“數字語言”(特征向量)。語言模型像一個“講故事的人”,根據文本和 CLIP 提供的圖像信息,生成答案或描述。
git clone https://www.modelscope.cn/models/openai-mirror/clip-vit-base-patch16
- CLIP:由 OpenAI 開發的全稱是 “Contrastive Language-Image Pretraining”(對比式語言-圖像預訓練)。它是一個多模態模型,能夠同時理解圖像和文本,通過在大規模圖文對數據上預訓練,學會將圖像和文本映射到同一個特征空間。
- ViT:表示 “Vision Transformer”,是 CLIP 的視覺部分,使用 Transformer 架構處理圖像。
- Base:表示模型規模中等(相比 Small 或 Large)。
- Patch16:表示將圖像分割成 16x16 像素的 patch(小塊),作為輸入處理。
下載純語言模型權重到 ./out 目錄下(作為訓練VLM的基座語言模型)
wget -c https://huggingface.co/jingyaogong/MiniMind2-V-PyTorch/blob/main/lm_512.pth
訓練
下載數據集
minimind-v_dataset
是一個由 Jingyao Gong 提供的數據集,主要用于機器學習和計算機視覺任務。以下是該數據集的主要特點:
- 模態:圖像
- 文件格式:imagefolder
- 數據集大小:小于 1K
- 相關文獻:包含兩篇 ArXiv 論文(arxiv: 2304.08485 和 arxiv: 2310.03744)
- 文件結構:
- 包含多個 JSONL 文件和壓縮的圖像文件,如
pretrain_images.zip
和sft_images.zip
。 - 還包括 README 文件,提供數據集的詳細說明。
- 包含多個 JSONL 文件和壓縮的圖像文件,如
該數據集適合用于預訓練和微調模型,特別是在視覺語言模型(VLM)相關的研究中。
from huggingface_hub import list_repo_files, hf_hub_download
import os# 數據集的 repo_id
repo_id = "jingyaogong/minimind-v_dataset"
save_dir = "./dataset"# 創建保存目錄
os.makedirs(save_dir, exist_ok=True)# 獲取數據集中的所有文件列表
files = list_repo_files(repo_id=repo_id, repo_type="dataset")
print("Found files:", files)# 下載每個文件
for file in files:print(f"Downloading {file}...")hf_hub_download(repo_id=repo_id,filename=file,repo_type="dataset",local_dir=save_dir,local_dir_use_symlinks=False # 直接保存文件,避免符號鏈接)print(f"Saved {file} to {save_dir}")print("All files downloaded!")
開啟訓練
?預訓練(學圖像描述)
預訓練從595K條數據集中學習圖片的通用知識,比如鹿是鹿,狗是狗。
torchrun --nproc_per_node 2 train_pretrain_vlm.py --epochs 4
監督微調(學看圖對話方式)
指令微調從300K條真實對話數據集中學習對圖片提問的真實問答格式,更符合與人類的交流習慣。訓練時均凍結visual encoder也就是clip模型梯度, 只訓練Projection和LLM兩部分。 預訓練中,只設置Projection和LLM的最后一層參數可學習。 指令微調中,設置Projection和LLM的全部參數可學習。SFT 代碼里把下面注釋代碼放開凍結參數
# 只解凍注意力機制中的投影層參數for name, param in model.model.named_parameters():if any(proj in name for proj in ['q_proj', 'k_proj', 'v_proj', 'o_proj']):param.requires_grad = True
實際上作者是在保存模型權重的時候過濾 vision_encoder 參數
if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0):model.eval()moe_path = '_moe' if model_config.use_moe else ''...clean_state_dict = {key: value for key, value in state_dict.items() if not key.startswith('vision_encoder.')}clean_state_dict = {k: v.half() for k, v in clean_state_dict.items()} # 半精度保存torch.save(clean_state_dict, ckp)
- 保存模型權重時,顯式過濾掉以 vision_encoder. 開頭的參數(即 CLIP 模型的參數)。
- 這表明開發者假設 CLIP 模型的參數不需要保存,可能因為它們在訓練中保持不變(凍結)或直接使用預訓練權重。
-
雖然訓練代碼沒有凍結 vision_encoder,但保存權重時過濾 vision_encoder 參數,暗示開發者可能預期 CLIP 模型不參與訓練,或者認為它的權重不需要更新。
為什么需要凍結 CLIP 模型?
- 預訓練優勢:CLIP 模型(clip-vit-base-patch16)已經在大型圖文數據集上預訓練,特征提取能力強,無需進一步微調。
- 計算效率:CLIP 模型參數量較大(約 8600 萬),凍結它可以減少顯存占用和計算量,加速訓練(MiniMind-V 強調輕量高效)。
- 任務需求:監督微調(SFT)階段主要優化語言生成能力(LLM)和圖文對齊(Projection),CLIP 的視覺特征通常保持不變。
模型轉換
if __name__ == '__main__':lm_config = VLMConfig(hidden_size=512, num_hidden_layers=16, max_seq_len=8192, use_moe=False)torch_path = f"../out/sft_vlm_{lm_config.hidden_size}{'_moe' if lm_config.use_moe else ''}.pth"transformers_path = '../MiniMind2-V'convert_torch2transformers_minimind(torch_path, transformers_path)
運行 web_demo
Transformers 庫通過以下機制支持圖像和文本的多模態功能(以 MiniMind-V 和 _generate 方法為上下文):model.generate 來生成文本輸出,結合了文本(input_ids)和圖像(pixel_values)輸入,體現了一種多模態生成能力
model.generate(inputs.input_ids,max_new_tokens=args.max_seq_len,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,pixel_values=pixel_values)