模型構建
# coding=utf-8
"""
BERT 三大任務演示 - 簡化版
功能:
1. 情感分析 - 判斷文本正面/負面情緒
2. 命名實體識別 - 識別人名、地名、組織等
3. 問答系統 - 從文本中提取答案

使用離線模型,便於學習和理解
"""

import time
import warnings
warnings.filterwarnings('ignore')


# ============================================================================
# 配置:本地模型路徑
# ============================================================================
MODEL_PATHS = {
    'sentiment': 'C:/Users/PC/Desktop/models/sentiment-analysis',      # 情感分析模型
    'ner': 'C:/Users/PC/Desktop/models/ner',                           # 命名實體識別模型
    'qa': 'C:/Users/PC/Desktop/models/qa'                              # 問答模型
}


# ============================================================================
# 工具函數:檢查環境
# ============================================================================
def check_environment():
    """檢查Python環境和依賴庫"""
    try:
        import transformers
        import torch

        print(f"✓ transformers 版本: {transformers.__version__}")
        print(f"✓ torch 版本: {torch.__version__}")

        # 檢查是否有GPU
        if torch.cuda.is_available():
            print(f"GPU 可用: {torch.cuda.get_device_name(0)}")
        else:
            print("使用 CPU 模式")

        print()
        return True

    except ImportError as e:
        print(f"缺少必要的庫: {e}")
        print("\n請安裝依賴:")
        print("  pip install transformers torch")
        return False


# 任務1:情感分析
class SentimentAnalyzer:
    """
    情感分析器
    功能:判斷文本的情感傾向(正面或負面)
    應用:電商評論分析、輿情監控等
    """
    
    def __init__(self, model_path):
        """
        初始化模型
        參數:
            model_path: 本地模型文件夾路徑
            AutoTokenizer:分詞器:導入的是代碼框架(tokenize、encode、decode等);from_pretrained(model_path):加載預訓練數據(詞彙表、配置文件具體參數等)
            AutoModelForSequenceClassification:BERT模型:
            torch:PyTorch深度學習框架
        """
        from transformers import AutoTokenizer, AutoModelForSequenceClassification
        import torch
        
        print("【加載情感分析模型】")
        print(f"模型路徑: {model_path}\n")

        # 從本地路徑加載預訓練的分詞器 (Tokenizer)/ˈtoʊ.kən.aɪ.zɚ/;Token/ˈtəʊ.kən/:(包含詞彙表、特殊符號等)
        # 分詞器功能:1、文本拆分成tokens(字詞);2、將tokens(字詞)轉換為對應固定數字ID(模型識別數字)
        self.tokenizer = AutoTokenizer.from_pretrained(model_path)

        # 從本地加載情感分類模型(預訓練的BERT + 分類頭)
        self.model = AutoModelForSequenceClassification.from_pretrained(model_path)
        
        # 將模型設置為評估模式(不dropout以及不更新梯度)
        self.model.eval() 

        # 設置運行設備(GPU快於CPU,有GPU則用GPU)
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.model.to(self.device)

        # 標籤映射
        self.labels = {0: "負面 ", 1: "正面 "}

        print("模型加載成功\n")

    def predict(self, text):
        """
        預測文本情感
        參數:
            text: 輸入文本
        返回:
            包含預測結果的字典
        """
        import torch

        # 1. 調用tokenizer進行文本預處理(分詞、轉ID、padding等)
        inputs = self.tokenizer(
            text,
            return_tensors='pt',      # 返回PyTorch張量
            padding=True,              # 自動padding
            truncation=True,           # 超長截斷
            max_length=512             # 最大長度
        )

        # 2. 嘗試將輸入移到對應設備提高運行速率(轉移前(CPU)→  轉移後(GPU))
        inputs = {k: v.to(self.device) for k, v in inputs.items()}

        # 3. torch.no_grad():預測模式,不計算梯度,輸出的outputs包含原始得分
        with torch.no_grad():
            outputs = self.model(**inputs)

        # 4. 將logits轉換為概率:outputs.logits是原始得分tensor([[-3.2156,  4.8234]]),Softmax:將其映射到 [0, 1],概率和為 1
        probs = torch.softmax(outputs.logits, dim=-1)

        # 5. 獲取預測類別:tensor([[0.0003, 0.9997]]):找最大值的索引 → 轉為整數(1)
        pred_id = torch.argmax(probs, dim=-1).item()

        # 6. 返回結果
        return {
            'text': text,                            # 這家餐廳非常好
            'label': self.labels[pred_id],           # '正面 '
            'confidence': probs[0][pred_id].item(),  # 0.9987
            'all_scores': {
                self.labels[i]: probs[0][i].item()   # {'負面 ': 0.0013,
                for i in range(len(self.labels))     # '正面 ': 0.9987 }
            }
        }



# 任務2:命名實體識別
class NamedEntityRecognizer:
    """
    命名實體識別器(NER)
    功能:識別文本中的人名、地名、組織機構等
    應用:信息抽取、知識圖譜構建等
    """

    def __init__(self, model_path):
        """
        初始化模型
        參數:
            model_path: 本地模型文件夾路徑
        """
        # AutoModelForTokenClassification:Token分類模型(每個token字詞預測一個標籤)
        from transformers import AutoTokenizer, AutoModelForTokenClassification
        import torch

        print("【加載命名實體識別模型】")
        print(f"模型路徑: {model_path}\n")

        # 加載分詞器:use_fast=True:使用Rust實現的快速分詞器(支持 return_offsets_mapping)
        self.tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=True)

        # 加載Token字詞分類模型:BERT + 分類頭(每個token輸出實體類別)
        self.model = AutoModelForTokenClassification.from_pretrained(model_path)

        # .eval():評估模式
        self.model.eval()

        # 設置運行設備
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.model.to(self.device)

        # ID到實體標籤的映射,比如
        # {1: 'B-PER',    # Begin-Person(人名開始)
        # 2: 'I-PER',    # Inside-Person(人名延續)}
        self.id2label = self.model.config.id2label

        print("✓ 模型加載成功\n")

    def predict(self, text):
        """
        識別文本中的命名實體
        參數:
            text: 輸入文本
        返回:
            包含實體列表的字典
        """
        import torch

        # 步驟1:文本預處理(分詞 + 位置記錄)
        inputs = self.tokenizer(
            text,
            return_tensors='pt',
            padding=True,
            truncation=True,
            max_length=512,
            return_offsets_mapping=True  # 返回每個token在原文中的位置
        )

        # 步驟2:提取位置映射並將其移除:模型不需要offset_mapping
        offset_mapping = inputs.pop('offset_mapping')[0]

        # 步驟3. 將輸入移到設備
        inputs = {k: v.to(self.device) for k, v in inputs.items()}

        # 步驟4. 模型預測
        with torch.no_grad():
            outputs = self.model(**inputs)

        # 步驟5. 找出每個token最可能的標籤對應索引:predictions = [0, 1, 2...],比如 1 對應 'B-PER'(人名開始)
        predictions = torch.argmax(outputs.logits, dim=-1)[0]

        # 6. 解析實體:BIO標註
        # 輸入: 每個token的標籤預測(如:['O', 'B-PER', 'I-PER', 'O', 'B-LOC', 'I-LOC'])
        # 輸出: 實體列表(如:[{'text': '王明', 'type': 'PER'}, {'text': '北京', 'type': 'LOC'}])

        entities = [] # 存儲所有識別出的實體
        current_entity = None # 當前正在構建的實體(狀態機)

        # 遍歷所有token字詞並獲取標籤:比如pred_id = 1(對應'B-PER') , offset = (0,1): 表示[token在原文中的起始位置, token在原文中的結束位置)
        for pred_id, offset in zip(predictions, offset_mapping):
            
            # 跳過特殊token:[CLS]、[SEP]、[PAD] 等特殊token的offset是 (0, 0),不需要處理
            if offset[0] == 0 and offset[1] == 0:
                continue

            # 用數字索引去查字典, 比如 pred_id = 1,self.id2label = {0: 'O', 1: 'B-PER',...},label = 'B-PER' (人名開始)
            label = self.id2label[pred_id.item()]

            # 情況一:B-開頭:新實體開始(begin)
            if label.startswith('B-'):
                if current_entity: # 如果有未完成的實體,先保存
                    entities.append(current_entity)
                current_entity = { # 創建新實體
                    'text': text[offset[0]:offset[1]], # 提取文本片段
                    'type': label[2:], # 去掉'B-'前綴
                    'start': offset[0].item(), # 起始位置
                    'end': offset[1].item() # 結束位置
                }

            # I-開頭:實體延續(inside)
            elif label.startswith('I-'):

                # 檢查是否有正在構建的current_entity實體;檢查label[2:]類型是否匹配
                if current_entity and label[2:] == current_entity['type']:

                    # 原文中提取完整的實體文本
                    # current_entity['start']:使用原來的起始位置(不變)
                    # offset[1]:使用當前token的結束位置(擴展),比如 text[0:2] = '王明'
                    current_entity['text'] = text[current_entity['start']:offset[1]]
                    current_entity['end'] = offset[1].item() # 更新結束位置

            # O:非實體
            else:
                if current_entity:
                    entities.append(current_entity)
                    current_entity = None

        # 添加最後一個實體
        if current_entity:
            entities.append(current_entity)

        return {
            'text': text,
            'entities': entities
        }


# 任務3:問答系統
class QuestionAnsweringSystem:
    """
    抽取式問答系統:
    輸入:問題 + 包含答案的文本(上下文)
    輸出:從上下文中抽取出的答案
    應用:智能客服、搜索引擎等
    """

    def __init__(self, model_path):
        """
        初始化模型
        參數:
            model_path: 本地模型文件夾路徑
        """
        # AutoModelForQuestionAnswering:兩個輸出頭:start_logits(起始位置)和 end_logits(結束位置):足夠預測任意長度的答案
        from transformers import AutoTokenizer, AutoModelForQuestionAnswering
        import torch

        print("【加載問答模型】")
        print(f"模型路徑: {model_path}\n")

        # 加載分詞器和模型
        self.tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=True)
        self.model = AutoModelForQuestionAnswering.from_pretrained(model_path)
        self.model.eval()

        # 設置運行設備
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.model.to(self.device)

        print(" 模型加載成功\n")

    def answer(self, question, context):
        import torch

        # 1. 將問題和上下文兩個文本同時編碼為模型可以理解的數字格式
        inputs = self.tokenizer(
            question, # 第1個文本:問題
            context,  # 第2個文本:上下文
            return_tensors='pt', # 指定返回 PyTorch 張量格式
            padding=True, # 自動填充到批次中最長序列的長度max_length
            truncation=True, # 序列超過 max_length,自動截斷
            max_length=512  # 設置最大序列長度為512
        )

        # 2. 將輸入移到設備
        inputs = {k: v.to(self.device) for k, v in inputs.items()}

        # 3. 模型預測(預測答案的起始和結束位置)
        with torch.no_grad(): # 告訴PyTorch 不計算梯度(預測模式)
            
            # 模型前向傳播:輸出的outputs包含start_logits(起始位置)和end_logits(結束位置)
            outputs = self.model(**inputs) 

        # 4. 找到分數最高的位置索引來獲取起始和結束位置
        start_idx = torch.argmax(outputs.start_logits).item()
        end_idx = torch.argmax(outputs.end_logits).item()

        # 5. 確保結束位置不在起始位置之前,否則將結束位置設為起始位置,返回單個 token 作為答案
        if end_idx < start_idx:
            end_idx = start_idx

        # 6. 計算置信度
        # 將所有位置的 logits 轉換為概率分佈(總和為1)
        start_score = torch.softmax(outputs.start_logits[0], dim=-1)[start_idx].item() # start_probs[10] = 0.957

        end_score = torch.softmax(outputs.end_logits[0], dim=-1)[end_idx].item() # end_probs[11] = 0.964
        
        confidence = (start_score + end_score) / 2 # (0.957 + 0.964) / 2

        # 7. [start_idx:end_idx+1] :切片提取答案部分對應的ID:[1266, 776]
        answer_tokens = inputs['input_ids'][0][start_idx:end_idx+1]

        # 舉例:[1266, 776] →(skip_special_tokens跳過特殊標記): '北京'
        answer_text = self.tokenizer.decode(answer_tokens, skip_special_tokens=True)

        # 8. 處理無答案情況
        if not answer_text.strip():
            return {
                'question': question,
                'context': context,
                'answer': " 無法找到答案",
                'confidence': 0.0
            }

        return {
            'question': question,
            'context': context,
            'answer': answer_text,
            'confidence': confidence
        }
調用模型
def demo_sentiment():
    """演示情感分析"""
    try:
        #  創建 SentimentAnalyzer 對象,傳入模型路徑,這一步加載 BERT 模型和分詞器
        analyzer = SentimentAnalyzer(MODEL_PATHS['sentiment'])

        # 測試文本
        test_texts = [
            "這家餐廳的菜品非常好吃,服務態度也很棒!",
            "價格太貴了,性價比很低,不推薦",
            "環境很好,但是菜品一般般",
            "超級好吃!強烈推薦給大家!",
            "服務態度極差,再也不會來了"
        ]

        # 逐個預測
        for i, text in enumerate(test_texts, 1): # 給每個文本編號,從1開始
            print(f" 文本 {i}: {text}")

            # 調用模型預測
            result = analyzer.predict(text)

            print(f"   預測結果: {result['label']}")
            print(f"   置信度: {result['confidence']:.2%}")

    except Exception as e:
        print(f"✗ 演示失敗: {e}")
        print("請檢查模型路徑是否正確\n")


def demo_ner():
    """演示命名實體識別"""
    try:
        # 創建命名實體識別對象
        recognizer = NamedEntityRecognizer(MODEL_PATHS['ner'])

        # 測試文本
        test_texts = [
            "王明在北京大學學習計算機科學",
            "馬化騰是騰訊公司的創始人",
            "2024年3月,李華在上海舉辦了畫展",
            "張偉博士在清華大學擔任教授"
        ]

        # 逐個識別
        for i, text in enumerate(test_texts, 1):
            print(f"文本 {i}: {text}")

            result = recognizer.predict(text)

            if result['entities']:
                print(f"   識別到 {len(result['entities'])} 個實體:")
                for entity in result['entities']:
                    
                    # 格式化輸出(填充空格和截斷長文本)
                    print(f"      • {entity['text']:<10} → [{entity['type']}]")
            else:
                print("   未識別到實體")

            print()

        print("✓ 命名實體識別演示完成\n")

    except Exception as e:
        print(f"演示失敗: {e}")
        print("請檢查模型路徑是否正確\n")


def demo_qa():
    """演示問答系統"""
    try:
        # 初始化模型
        qa_system = QuestionAnsweringSystem(MODEL_PATHS['qa'])

        # 測試問答對
        test_cases = [
            {
                'context': 'BERT是Google在2018年提出的預訓練語言模型。它的全稱是Bidirectional Encoder Representations from Transformers。BERT在多個自然語言處理任務上都取得了突破性的成果。',
                'question': 'BERT是什麼時候提出的?'
            },
            {
                'context': '北京是中國的首都,',
                'question': '中國的首都是哪兒?'
            },
            {
                'context': '深度學習是機器學習的一個分支,它使用多層神經網絡來學習數據的表示。深度學習在計算機視覺、語音識別和自然語言處理等領域都有廣泛應用。',
                'question': '深度學習在哪些領域有應用?'
            },
            {
                'context': '張大彪非常喜歡打羽毛球',
                'question': '誰非常喜歡打羽毛球?'
            }
        ]

        # 逐個問答
        for i, case in enumerate(test_cases, 1):
            print(f"問答 {i}:")
            print(f"   上下文: {case['context']}")
            print(f"   問題: {case['question']}")

            
            # qa_system.answer() 內部會:1. 編碼:將問題和上下文轉為 token IDs → 2. 預測:模型輸出 start_logits 和 end_logits
            # → 3. 定位:找到答案的起始和結束位置 → 4. 解碼:將 token IDs 轉換回文本 → 5. 返回結果字典
            result = qa_system.answer(case['question'], case['context'])

            print(f"   答案: {result['answer']}")
            print(f"   置信度: {result['confidence']:.2%}")
            print()

        print("✓ 問答系統演示完成\n")

    except Exception as e:
        print(f"✗ 演示失敗: {e}")
        print("請檢查模型路徑是否正確\n")



# 主程序

def main():
    """主函數:運行所有演示"""

    print("        BERT 三大任務演示 - 簡化版")
    print("\n 包含任務:")
    print("   1. 情感分析")
    print("   2. 命名實體識別")
    print("   3. 問答系統")
    print("\n 模式: 離線模式(使用本地模型)")
    print("\n 模型路徑:")
    for task, path in MODEL_PATHS.items():
        print(f"   {task}: {path}")
    print("="*70)

    # 檢查環境
    if not check_environment():
        return

    # 運行三個演示
    demo_sentiment()      # 任務1:情感分析
    demo_ner()            # 任務2:命名實體識別
    demo_qa()             # 任務3:問答系統


if __name__ == "__main__":
        main()
BERT 三大任務演示 - 簡化版

 包含任務:
   1. 情感分析
   2. 命名實體識別
   3. 問答系統

 模式: 離線模式(使用本地模型)

 模型路徑:
   sentiment: C:/Users/PC/Desktop/models/sentiment-analysis
   ner: C:/Users/PC/Desktop/models/ner
   qa: C:/Users/PC/Desktop/models/qa
======================================================================
✓ transformers 版本: 4.51.1
✓ torch 版本: 2.6.0+cpu
使用 CPU 模式

【加載情感分析模型】
模型路徑: C:/Users/PC/Desktop/models/sentiment-analysis

模型加載成功

 文本 1: 這家餐廳的菜品非常好吃,服務態度也很棒!
   預測結果: 正面 
   置信度: 98.31%
 文本 2: 價格太貴了,性價比很低,不推薦
   預測結果: 負面 
   置信度: 98.19%
 文本 3: 環境很好,但是菜品一般般
   預測結果: 負面 
   置信度: 61.52%
 文本 4: 超級好吃!強烈推薦給大家!
   預測結果: 正面 
   置信度: 97.42%
 文本 5: 服務態度極差,再也不會來了
   預測結果: 負面 
   置信度: 99.63%
【加載命名實體識別模型】
模型路徑: C:/Users/PC/Desktop/models/ner

✓ 模型加載成功

文本 1: 王明在北京大學學習計算機科學
   識別到 2 個實體:
      • 王明         → [name]
      • 北京大學       → [organization]

文本 2: 馬化騰是騰訊公司的創始人
   識別到 3 個實體:
      • 馬化騰        → [name]
      • 騰訊公司       → [company]
      • 創始人        → [position]

文本 3: 2024年3月,李華在上海舉辦了畫展
   識別到 2 個實體:
      • 李華         → [name]
      • 上海         → [address]

文本 4: 張偉博士在清華大學擔任教授
   識別到 4 個實體:
      • 張偉         → [name]
      • 博士         → [position]
      • 清華大學       → [organization]
      • 教授         → [position]

✓ 命名實體識別演示完成

【加載問答模型】
模型路徑: C:/Users/PC/Desktop/models/qa

 模型加載成功

問答 1:
   上下文: BERT是Google在2018年提出的預訓練語言模型。它的全稱是Bidirectional Encoder Representations from Transformers。BERT在多個自然語言處理任務上都取得了突破性的成果。
   問題: BERT是什麼時候提出的?
   答案: 2018 年
   置信度: 80.47%

問答 2:
   上下文: 北京是中國的首都,
   問題: 中國的首都是哪兒?
   答案: 北 京
   置信度: 72.87%

問答 3:
   上下文: 深度學習是機器學習的一個分支,它使用多層神經網絡來學習數據的表示。深度學習在計算機視覺、語音識別和自然語言處理等領域都有廣泛應用。
   問題: 深度學習在哪些領域有應用?
   答案: 計 算 機 視 覺 、 語 音 識 別 和 自 然 語 言 處 理 等 領 域
   置信度: 73.05%

問答 4:
   上下文: 張大彪非常喜歡打羽毛球
   問題: 誰非常喜歡打羽毛球?
   答案: 張 大 彪
   置信度: 91.61%

✓ 問答系統演示完成