系統(tǒng):從模型到前后端落地)
簡(jiǎn)介本資源為基于BERT的深度學(xué)習(xí)文本相似度檢測(cè)系統(tǒng)畢業(yè)設(shè)計(jì)源碼包面向計(jì)算機(jī)相關(guān)專業(yè)學(xué)生及NLP入門開發(fā)者可用于畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)或文本語(yǔ)義匹配實(shí)踐。系統(tǒng)以Python 3.6.8開發(fā)采用BERT預(yù)訓(xùn)練模型完成語(yǔ)義理解與相似度評(píng)估后端結(jié)合MySQL 5.7存儲(chǔ)數(shù)據(jù)前端頁(yè)面與PyCharm開發(fā)環(huán)境配套覆蓋搜索引擎、自動(dòng)問(wèn)答、推薦系統(tǒng)等典型應(yīng)用場(chǎng)景。壓縮包共389個(gè)文件約52.19MB包含72個(gè)py源碼、35個(gè)js腳本、15個(gè)html頁(yè)面及18個(gè)css樣式文件另有數(shù)據(jù)庫(kù)sql、說(shuō)明文檔docx與pdf、模型相關(guān)zip及圖片資源前后端與數(shù)據(jù)庫(kù)結(jié)構(gòu)完整。項(xiàng)目說(shuō)明文檔記錄了開發(fā)背景、設(shè)計(jì)思路、系統(tǒng)架構(gòu)與運(yùn)行環(huán)境源碼目錄便于理解實(shí)現(xiàn)機(jī)制數(shù)據(jù)庫(kù)文件提供建表與數(shù)據(jù)基礎(chǔ)。目前已有78人學(xué)習(xí)適合需要完整賽題方案、排錯(cuò)思路與目錄結(jié)構(gòu)參考的讀者。1. 文本相似度檢測(cè)系統(tǒng)從 BERT 到前后端落地的完整路徑很多做畢業(yè)設(shè)計(jì)的同學(xué)一看到文本相似度檢測(cè)就覺得無(wú)非是算個(gè)余弦相似度但真正動(dòng)手才發(fā)現(xiàn)兩個(gè)句子長(zhǎng)度不一樣怎么辦同義詞替換后相似度驟降怎么處理前端怎么把一段文本傳給后端模型MySQL 里到底存什么這些問(wèn)題不解決系統(tǒng)就跑不起來(lái)。這個(gè)標(biāo)題指向的是一套完整的工程方案——用 BERT 做語(yǔ)義向量化配合 Python 后端提供推理接口前端做交互展示MySQL 存儲(chǔ)檢測(cè)記錄最終交付一個(gè)可演示、可復(fù)現(xiàn)的文本相似度檢測(cè)系統(tǒng)。它適合正在做 NLP 方向畢業(yè)設(shè)計(jì)、需要一套能跑通全流程參考方案的開發(fā)者也適合想理解模型怎么變成系統(tǒng)這個(gè)跨越過(guò)程的同學(xué)。接下來(lái)我會(huì)把選型理由、代碼實(shí)現(xiàn)、參數(shù)配置和踩坑經(jīng)驗(yàn)逐層拆開講。2. BERT 做文本相似度的原理與模型選型為什么不用 TF-IDF2.1 從詞袋模型到語(yǔ)義向量的本質(zhì)差異TF-IDF 和 Word2Vec 是很多人最先接觸的方案。TF-IDF 把文本當(dāng)成詞頻統(tǒng)計(jì)向量?jī)蓚€(gè)句子只要用詞不同哪怕意思一樣相似度也會(huì)很低。比如如何學(xué)習(xí)編程和怎樣入門寫代碼TF-IDF 算出來(lái)幾乎不相關(guān)。Word2Vec 稍好一些詞級(jí)別有語(yǔ)義但它是靜態(tài)詞向量——蘋果在吃蘋果和蘋果發(fā)布會(huì)里向量完全一樣句子級(jí)別的語(yǔ)義就丟失了。BERT 的核心優(yōu)勢(shì)在于它是動(dòng)態(tài)編碼的。同一個(gè)詞在不同上下文里產(chǎn)生的向量不同而且 BERT 輸出的[CLS]token 向量或者池化后的句向量天然攜帶整句語(yǔ)義。做相似度檢測(cè)時(shí)把兩個(gè)句子分別編碼成固定維度向量再算余弦相似度就能捕捉到表述不同但含義接近的情況。常見做法是取 BERT 最后一層的[CLS]向量做句表示或者對(duì)所有 token 的輸出做 mean pooling。實(shí)踐中 mean pooling 在相似度任務(wù)上通常比[CLS]更穩(wěn)因?yàn)閇CLS]在預(yù)訓(xùn)練時(shí)主要服務(wù)于分類任務(wù)未必是句語(yǔ)義的最優(yōu)摘要。2.2 模型選型中文場(chǎng)景下的實(shí)際考量做中文文本相似度模型選擇直接決定效果上限。以下是幾種常見方案的實(shí)際對(duì)比模型參數(shù)量中文效果推理速度CPU適用場(chǎng)景bert-base-chinese110M良好約 200ms/對(duì)畢設(shè)首選資料多bert-base-multilingual-cased178M一般約 350ms/對(duì)需要多語(yǔ)言時(shí)輕量級(jí)蒸餾模型60M左右中等約 80ms/對(duì)對(duì)速度敏感時(shí)領(lǐng)域微調(diào)后的 BERT110M優(yōu)秀約 200ms/對(duì)有標(biāo)注數(shù)據(jù)時(shí)對(duì)于畢業(yè)設(shè)計(jì)場(chǎng)景我一般建議從bert-base-chinese起步。原因很直接中文預(yù)訓(xùn)練充分、HuggingFace 上文檔齊全、社區(qū)踩坑記錄多出了問(wèn)題容易搜到答案。如果答辯時(shí)被問(wèn)到為什么不用更大的模型可以從參數(shù)量與部署成本的平衡角度回答。注意不要一上來(lái)就追求 SOTA 效果。畢設(shè)的核心是展示完整的技術(shù)鏈路和工程能力模型選型合理、系統(tǒng)跑得通、有對(duì)比實(shí)驗(yàn)比盲目堆大模型更有說(shuō)服力。2.3 相似度計(jì)算方式的選擇拿到兩個(gè)句向量之后計(jì)算相似度有幾種方式余弦相似度最常用對(duì)向量長(zhǎng)度不敏感值域 [-1, 1]適合歸一化后的向量歐氏距離值域 [0, ∞)需要額外做歸一化映射到 [0,1]曼哈頓距離對(duì)異常維度更魯棒但解釋性不如余弦實(shí)際工程中余弦相似度是默認(rèn)選擇。代碼實(shí)現(xiàn)上先對(duì)向量做 L2 歸一化然后點(diǎn)積就是余弦相似度省去除法運(yùn)算import torch import torch.nn.functional as F from transformers import BertTokenizer, BertModel # 加載預(yù)訓(xùn)練模型和分詞器 model_name bert-base-chinese tokenizer BertTokenizer.from_pretrained(model_name) model BertModel.from_pretrained(model_name) model.eval() # 推理模式關(guān)閉 dropout def get_sentence_embedding(text): 將單句文本編碼為固定維度向量使用 mean pooling inputs tokenizer( text, return_tensorspt, paddingTrue, truncationTrue, max_length128 # 超過(guò)128個(gè)token截?cái)?) with torch.no_grad(): # 不計(jì)算梯度節(jié)省內(nèi)存 outputs model(**inputs) # mean pooling對(duì)所有token的輸出取平均排除padding位置 attention_mask inputs[attention_mask] token_embeddings outputs.last_hidden_state # 擴(kuò)展mask維度以匹配embedding維度 mask_expanded attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() sum_embeddings torch.sum(token_embeddings * mask_expanded, dim1) sum_mask torch.clamp(mask_expanded.sum(dim1), min1e-9) sentence_embedding sum_embeddings / sum_mask # L2歸一化后續(xù)點(diǎn)積即余弦相似度 sentence_embedding F.normalize(sentence_embedding, p2, dim1) return sentence_embedding def compute_similarity(text_a, text_b): 計(jì)算兩段文本的余弦相似度 emb_a get_sentence_embedding(text_a) emb_b get_sentence_embedding(text_b) # 歸一化后點(diǎn)積等于余弦相似度 similarity torch.sum(emb_a * emb_b, dim1) return similarity.item() # 測(cè)試 score compute_similarity(如何學(xué)習(xí)編程, 怎樣入門寫代碼) print(f相似度: {score:.4f})這段代碼的關(guān)鍵點(diǎn)有三個(gè)。第一attention_mask的處理——如果不排除 padding 位置短句的向量會(huì)被大量零向量稀釋導(dǎo)致相似度計(jì)算偏差。第二max_length128是權(quán)衡太長(zhǎng)會(huì)拖慢推理太短會(huì)截?cái)嘈畔⒅形膱?chǎng)景下 128 個(gè) token 大約覆蓋 200 多個(gè)漢字對(duì)大多數(shù)句子夠用。第三F.normalize之后用點(diǎn)積代替余弦公式減少一次除法運(yùn)算批量計(jì)算時(shí)效率提升明顯。參數(shù)調(diào)整建議如果句子普遍較長(zhǎng)比如段落級(jí)比對(duì)把max_length提到 256 或 512但要注意顯存占用會(huì)平方級(jí)增長(zhǎng)。如果部署在 CPU 上且對(duì)延遲敏感可以降到 64犧牲一些長(zhǎng)句精度換速度。3. 后端服務(wù)搭建Flask 接口設(shè)計(jì)與模型加載策略3.1 項(xiàng)目結(jié)構(gòu)規(guī)劃一個(gè)清晰的項(xiàng)目結(jié)構(gòu)能讓后續(xù)維護(hù)和答辯演示都省心。我一般會(huì)這樣組織text_similarity_system/ ├── app.py # Flask 主入口 ├── model/ │ ├── __init__.py │ ├── encoder.py # BERT 編碼邏輯 │ └── similarity.py # 相似度計(jì)算 ├── db/ │ ├── __init__.py │ ├── models.py # SQLAlchemy 數(shù)據(jù)模型 │ └── init_db.py # 建表腳本 ├── static/ # 前端靜態(tài)資源 ├── templates/ # HTML 模板 ├── requirements.txt └── config.py # 配置參數(shù)模型加載放在應(yīng)用啟動(dòng)時(shí)完成不要每次請(qǐng)求都重新加載。BERT 模型加載一次大約需要 2-5 秒取決于磁盤速度如果放在請(qǐng)求處理函數(shù)里每次調(diào)用都要等這個(gè)時(shí)間用戶體驗(yàn)極差。3.2 Flask 接口實(shí)現(xiàn)from flask import Flask, request, jsonify, render_template from model.similarity import compute_similarity from db.models import db, SimilarityRecord import config app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] config.DATABASE_URI app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db.init_app(app) app.route(/) def index(): 渲染前端頁(yè)面 return render_template(index.html) app.route(/api/similarity, methods[POST]) def api_similarity(): 文本相似度檢測(cè)接口 data request.get_json() # 參數(shù)校驗(yàn) text_a data.get(text_a, ).strip() text_b data.get(text_b, ).strip() if not text_a or not text_b: return jsonify({code: 400, msg: 文本不能為空}), 400 if len(text_a) 500 or len(text_b) 500: return jsonify({code: 400, msg: 文本長(zhǎng)度不能超過(guò)500字}), 400 # 計(jì)算相似度 try: score compute_similarity(text_a, text_b) except Exception as e: return jsonify({code: 500, msg: f計(jì)算失敗: {str(e)}}), 500 # 判定等級(jí) if score 0.85: level 高度相似 elif score 0.65: level 中度相似 else: level 低度相似 # 寫入數(shù)據(jù)庫(kù) record SimilarityRecord( text_atext_a, text_btext_b, scoreround(score, 4), levellevel ) db.session.add(record) db.session.commit() return jsonify({ code: 200, data: { score: round(score, 4), level: level, record_id: record.id } }) app.route(/api/history, methods[GET]) def api_history(): 查詢歷史檢測(cè)記錄 page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) pagination SimilarityRecord.query.order_by( SimilarityRecord.created_at.desc() ).paginate(pagepage, per_pageper_page, error_outFalse) records [{ id: r.id, text_a: r.text_a[:50], # 截?cái)嗾故?text_b: r.text_b[:50], score: r.score, level: r.level, created_at: r.created_at.strftime(%Y-%m-%d %H:%M:%S) } for r in pagination.items] return jsonify({ code: 200, data: { records: records, total: pagination.total, pages: pagination.pages } }) if __name__ __main__: with app.app_context(): db.create_all() # 首次運(yùn)行自動(dòng)建表 app.run(host0.0.0.0, port5000, debugFalse)接口設(shè)計(jì)上有幾個(gè)值得注意的決策。第一相似度閾值 0.85 / 0.65 不是拍腦袋定的而是根據(jù)實(shí)際測(cè)試數(shù)據(jù)調(diào)整的——BERT 中文模型在語(yǔ)義相近的句對(duì)上通常給出 0.8 以上的分?jǐn)?shù)而完全不相關(guān)的句子一般在 0.3-0.5 之間。第二文本長(zhǎng)度限制 500 字是防止惡意長(zhǎng)文本拖垮服務(wù)同時(shí)也和前端輸入框的maxlength屬性保持一致。第三歷史記錄接口做了分頁(yè)避免一次性返回大量數(shù)據(jù)導(dǎo)致前端卡頓。3.3 數(shù)據(jù)庫(kù)表設(shè)計(jì)from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class SimilarityRecord(db.Model): 相似度檢測(cè)記錄表 __tablename__ similarity_record id db.Column(db.Integer, primary_keyTrue, autoincrementTrue) text_a db.Column(db.Text, nullableFalse, comment文本A) text_b db.Column(db.Text, nullableFalse, comment文本B) score db.Column(db.Float, nullableFalse, comment相似度分?jǐn)?shù)) level db.Column(db.String(20), nullableFalse, comment相似等級(jí)) created_at db.Column(db.DateTime, defaultdatetime.now, comment創(chuàng)建時(shí)間) def __repr__(self): return fRecord {self.id}: {self.score}表結(jié)構(gòu)保持簡(jiǎn)單只存必要字段。text類型用Text而不是String(255)因?yàn)橛脩艨赡茌斎胼^長(zhǎng)文本。created_at用datetime.now而不是datetime.utcnow因?yàn)楫呍O(shè)演示通常面向國(guó)內(nèi)用戶本地時(shí)間更直觀。注意MySQL 建庫(kù)時(shí)字符集務(wù)必用utf8mb4否則遇到 emoji 或生僻字會(huì)報(bào)錯(cuò)。建庫(kù)語(yǔ)句CREATE DATABASE similarity_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;4. 前端交互與聯(lián)調(diào)從輸入框到結(jié)果展示的完整鏈路4.1 頁(yè)面結(jié)構(gòu)與交互邏輯前端不需要太復(fù)雜核心是一個(gè)輸入?yún)^(qū)、一個(gè)結(jié)果展示區(qū)、一個(gè)歷史記錄列表。用原生 HTML CSS JavaScript 就夠了不必引入 Vue 或 React——畢設(shè)答辯時(shí)老師更關(guān)心功能是否完整而不是用了什么框架。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title文本相似度檢測(cè)系統(tǒng)/title style .container { max-width: 800px; margin: 40px auto; padding: 0 20px; } .input-group { margin-bottom: 16px; } textarea { width: 100%; height: 100px; padding: 10px; border: 1px solid #ddd; border-radius: 6px; resize: vertical; } .btn { padding: 10px 32px; background: #4a6cf7; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; } .btn:disabled { background: #aaa; cursor: not-allowed; } .result { margin-top: 24px; padding: 20px; background: #f5f7ff; border-radius: 8px; display: none; } .score { font-size: 36px; font-weight: bold; color: #4a6cf7; } .level { font-size: 18px; margin-top: 8px; } .history { margin-top: 40px; } .history table { width: 100%; border-collapse: collapse; } .history th, .history td { padding: 10px; border-bottom: 1px solid #eee; text-align: left; font-size: 14px; } /style /head body div classcontainer h1文本相似度檢測(cè)/h1 div classinput-group textarea idtextA placeholder請(qǐng)輸入文本A maxlength500/textarea /div div classinput-group textarea idtextB placeholder請(qǐng)輸入文本B maxlength500/textarea /div button classbtn idsubmitBtn onclickcheckSimilarity()開始檢測(cè)/button div classresult idresult div classscore idscoreValue--/div div classlevel idlevelValue--/div /div div classhistory h2歷史記錄/h2 table thead trth文本A/thth文本B/thth相似度/thth等級(jí)/thth時(shí)間/th/tr /thead tbody idhistoryBody/tbody /table /div /div script async function checkSimilarity() { const textA document.getElementById(textA).value.trim(); const textB document.getElementById(textB).value.trim(); if (!textA || !textB) { alert(請(qǐng)?zhí)顚憙啥挝谋?; return; } const btn document.getElementById(submitBtn); btn.disabled true; btn.textContent 檢測(cè)中...; try { const resp await fetch(/api/similarity, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ text_a: textA, text_b: textB }) }); const data await resp.json(); if (data.code 200) { document.getElementById(result).style.display block; document.getElementById(scoreValue).textContent (data.data.score * 100).toFixed(1) %; document.getElementById(levelValue).textContent data.data.level; loadHistory(); // 刷新歷史記錄 } else { alert(data.msg || 檢測(cè)失敗); } } catch (err) { alert(網(wǎng)絡(luò)錯(cuò)誤請(qǐng)稍后重試); } finally { btn.disabled false; btn.textContent 開始檢測(cè); } } async function loadHistory() { const resp await fetch(/api/history?page1per_page10); const data await resp.json(); if (data.code ! 200) return; const tbody document.getElementById(historyBody); tbody.innerHTML data.data.records.map(r tr td${r.text_a}/td td${r.text_b}/td td${(r.score * 100).toFixed(1)}%/td td${r.level}/td td${r.created_at}/td /tr ).join(); } // 頁(yè)面加載時(shí)拉取歷史記錄 loadHistory(); /script /body /html前端邏輯很直白用戶輸入兩段文本點(diǎn)擊按鈕后 POST 到后端接口拿到相似度分?jǐn)?shù)和等級(jí)后展示同時(shí)刷新歷史記錄表格。按鈕在請(qǐng)求期間禁用防止重復(fù)提交。maxlength500和后端校驗(yàn)保持一致避免用戶輸入超長(zhǎng)文本后被后端拒絕卻不知道為什么。4.2 聯(lián)調(diào)時(shí)最容易卡住的三個(gè)點(diǎn)第一個(gè)是跨域問(wèn)題。如果前端用 Live Server 跑在 5500 端口后端跑在 5000 端口瀏覽器會(huì)攔截請(qǐng)求。解決辦法有兩種一是后端加 CORS 頭二是把前端頁(yè)面直接放在 Flask 的templates目錄下由 Flask 渲染。畢設(shè)場(chǎng)景我推薦第二種省去跨域配置的麻煩。第二個(gè)是 JSON 解析失敗。Flask 的request.get_json()在請(qǐng)求頭沒(méi)有Content-Type: application/json時(shí)會(huì)返回None然后.get()就報(bào)錯(cuò)。前端fetch里必須顯式設(shè)置這個(gè)頭或者后端用request.get_json(forceTrue)強(qiáng)制解析。第三個(gè)是模型首次推理特別慢。BERT 模型第一次前向傳播時(shí)需要初始化一些計(jì)算圖結(jié)構(gòu)可能比后續(xù)推理慢 3-5 倍。解決辦法是在應(yīng)用啟動(dòng)后先跑一次預(yù)熱推理# 在 app.run 之前執(zhí)行一次預(yù)熱 with app.app_context(): db.create_all() # 預(yù)熱模型 _ compute_similarity(預(yù)熱文本, 預(yù)熱文本) print(模型預(yù)熱完成)5. 避坑與排查那些讓系統(tǒng)跑不起來(lái)的細(xì)節(jié)5.1 模型加載報(bào) OOM 或顯存不足現(xiàn)象啟動(dòng) Flask 時(shí)拋出RuntimeError: CUDA out of memory或者進(jìn)程被系統(tǒng) kill。原因BERT-base 模型加載到 GPU 大約占 400-500MB 顯存如果同時(shí)有其他進(jìn)程占用或者批量推理時(shí) batch size 設(shè)得太大就會(huì)溢出。解決畢設(shè)場(chǎng)景直接用 CPU 推理即可model.to(cpu)顯式指定。CPU 單次推理約 200ms對(duì)演示完全夠用。如果必須用 GPU把 batch size 設(shè)為 1并在推理后用torch.cuda.empty_cache()釋放緩存。5.2 相似度分?jǐn)?shù)普遍偏高或偏低現(xiàn)象任意兩句話算出來(lái)都在 0.7 以上或者明明意思相近卻只有 0.4。原因前者通常是沒(méi)用 mean pooling 而是直接取了[CLS]向量后者可能是max_length設(shè)得太小導(dǎo)致長(zhǎng)句被截?cái)嗷蛘邲](méi)有做 L2 歸一化。解決統(tǒng)一用 mean pooling L2 歸一化。如果分?jǐn)?shù)整體偏高檢查是否在model.eval()模式下推理——訓(xùn)練模式下的 dropout 會(huì)讓向量隨機(jī)波動(dòng)。如果分?jǐn)?shù)整體偏低把max_length從 128 提到 256 試試。5.3 MySQL 連接超時(shí)導(dǎo)致接口 500現(xiàn)象系統(tǒng)跑了一段時(shí)間后接口開始報(bào)OperationalError: (2006, MySQL server has gone away)。原因MySQL 默認(rèn)wait_timeout是 8 小時(shí)連接池里的空閑連接被服務(wù)端斷開但客戶端不知道繼續(xù)用舊連接就報(bào)錯(cuò)。解決在 SQLAlchemy 配置里加pool_pre_pingTrue每次從連接池取連接前先 ping 一下失效就重建app.config[SQLALCHEMY_ENGINE_OPTIONS] { pool_pre_ping: True, pool_recycle: 3600, # 1小時(shí)回收一次 pool_size: 5, max_overflow: 10 }5.4 中文輸入導(dǎo)致數(shù)據(jù)庫(kù)亂碼現(xiàn)象歷史記錄里中文顯示成???或亂碼。原因建庫(kù)時(shí)沒(méi)指定utf8mb4或者連接字符串沒(méi)加charset參數(shù)。解決連接字符串寫成mysqlpymysql://user:passhost/dbname?charsetutf8mb4同時(shí)確認(rèn)建庫(kù)語(yǔ)句用了CHARACTER SET utf8mb4。5.5 前端請(qǐng)求一直 pending 最后超時(shí)現(xiàn)象點(diǎn)擊檢測(cè)按鈕后轉(zhuǎn)圈很久最后報(bào)網(wǎng)絡(luò)錯(cuò)誤。原因Flask 默認(rèn)單線程處理請(qǐng)求如果模型推理阻塞了主線程后續(xù)請(qǐng)求全部排隊(duì)。開發(fā)模式下app.run()默認(rèn)threadedTrue但如果用了 gunicorn 且 worker 數(shù)為 1同樣會(huì)阻塞。解決開發(fā)時(shí)確認(rèn)app.run(threadedTrue)部署時(shí) gunicorn 至少開 2 個(gè) worker或者用--threads 4參數(shù)。更徹底的做法是把模型推理放到獨(dú)立進(jìn)程通過(guò)隊(duì)列通信但畢設(shè)場(chǎng)景沒(méi)必要做到這個(gè)程度。6. 進(jìn)階技巧讓相似度檢測(cè)更準(zhǔn)的幾個(gè)實(shí)用手段6.1 用對(duì)比學(xué)習(xí)微調(diào)提升領(lǐng)域效果預(yù)訓(xùn)練 BERT 在通用語(yǔ)料上表現(xiàn)不錯(cuò)但如果你的畢設(shè)場(chǎng)景是特定領(lǐng)域比如法律文書、醫(yī)療問(wèn)答通用模型的相似度判斷可能不夠準(zhǔn)。這時(shí)候可以用對(duì)比學(xué)習(xí)做微調(diào)構(gòu)造正樣本對(duì)語(yǔ)義相同和負(fù)樣本對(duì)語(yǔ)義不同用 triplet loss 或 cosine embedding loss 訓(xùn)練。from torch.optim import AdamW from transformers import get_linear_schedule_with_warmup # 假設(shè) train_dataloader 返回 (anchor, positive, negative) 三元組 optimizer AdamW(model.parameters(), lr2e-5, weight_decay0.01) scheduler get_linear_schedule_with_warmup( optimizer, num_warmup_steps100, num_training_steps1000 ) model.train() for epoch in range(3): for batch in train_dataloader: anchor_emb get_sentence_embedding(batch[anchor]) pos_emb get_sentence_embedding(batch[positive]) neg_emb get_sentence_embedding(batch[negative]) # triplet loss: 讓 anchor 與 positive 更近與 negative 更遠(yuǎn) pos_sim torch.sum(anchor_emb * pos_emb, dim1) neg_sim torch.sum(anchor_emb * neg_emb, dim1) loss torch.clamp(neg_sim - pos_sim 0.3, min0).mean() loss.backward() optimizer.step() scheduler.step() optimizer.zero_grad()關(guān)鍵參數(shù)學(xué)習(xí)率用 2e-5BERT 微調(diào)的標(biāo)準(zhǔn)起點(diǎn)margin 設(shè) 0.3表示正負(fù)樣本相似度至少差 0.3訓(xùn)練輪數(shù) 2-3 輪即可太多會(huì)過(guò)擬合。微調(diào)數(shù)據(jù)不需要太多每個(gè)類別幾百對(duì)就能看到明顯提升。6.2 緩存高頻查詢結(jié)果如果系統(tǒng)會(huì)被反復(fù)查詢相同的文本對(duì)加一層緩存能顯著降低響應(yīng)時(shí)間。用 Python 內(nèi)置的functools.lru_cache最簡(jiǎn)單from functools import lru_cache lru_cache(maxsize1000) def cached_similarity(text_a, text_b): 帶緩存的相似度計(jì)算相同輸入直接返回緩存結(jié)果 return compute_similarity(text_a, text_b)maxsize1000表示緩存最近 1000 組結(jié)果。注意這個(gè)裝飾器要求參數(shù)可哈希字符串天然滿足。如果文本很長(zhǎng)緩存會(huì)占內(nèi)存可以改用 Redis 做外部緩存并設(shè)置過(guò)期時(shí)間。6.3 批量推理提升吞吐當(dāng)需要一次性比對(duì)多組文本時(shí)逐對(duì)計(jì)算效率很低。把文本對(duì)拼成 batch 一起推理GPU 上能快 5-10 倍def batch_similarity(pairs, batch_size16): 批量計(jì)算文本對(duì)相似度 results [] for i in range(0, len(pairs), batch_size): batch pairs[i:ibatch_size] texts_a [p[0] for p in batch] texts_b [p[1] for p in batch] # 分別編碼兩組文本 emb_a get_sentence_embedding_batch(texts_a) emb_b get_sentence_embedding_batch(texts_b) # 逐對(duì)計(jì)算余弦相似度 sims torch.sum(emb_a * emb_b, dim1) results.extend(sims.tolist()) return resultsbatch_size根據(jù)顯存調(diào)整CPU 上建議 8-16GPU 上可以到 64 甚至 128。注意get_sentence_embedding_batch需要對(duì)整個(gè) batch 做 padding確保所有序列長(zhǎng)度一致。6.4 一個(gè)容易被忽略的驗(yàn)證方法答辯前一定要做一輪人工驗(yàn)證準(zhǔn)備 20-30 組文本對(duì)人工標(biāo)注相似/不相似然后跑系統(tǒng)看準(zhǔn)確率。如果準(zhǔn)確率低于 80%檢查是不是閾值設(shè)得不合理或者模型選型有問(wèn)題。這個(gè)驗(yàn)證過(guò)程本身也是答辯時(shí)的加分項(xiàng)——說(shuō)明你不僅跑通了系統(tǒng)還做了效果評(píng)估。我自己的習(xí)慣是每次改完模型或參數(shù)先跑這 30 組驗(yàn)證集確認(rèn)沒(méi)有退化再繼續(xù)。這個(gè)習(xí)慣幫我省了很多改了一個(gè)地方結(jié)果整體效果崩了卻不知道的后悔藥。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取