【日常训练】1200. 最小绝对差

本文介绍了一种算法,用于找出整数数组中具有最小绝对差的所有元素对,并按升序返回这些对。通过先排序数组,然后遍历数组来确定最小差值,最后收集并返回所有符合该最小差值的元素对。

题目

给你个整数数组 arr,其中每个元素都 不相同。

请你找到所有具有最小绝对差的元素对,并且按升序的顺序返回。

示例 1:
输入:arr = [4,2,1,3]
输出:[[1,2],[2,3],[3,4]]

示例 2:
输入:arr = [1,3,6,10,15]
输出:[[1,3]]

示例 3:
输入:arr = [3,8,-10,23,19,-4,-14,27]
输出:[[-14,-10],[19,23],[23,27]]

提示:
2 <= arr.length <= 105
-106 <= arr[i] <= 106

代码

package dayLeetCode;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class dayleetcode1200 {
    public List<List<Integer>> minimumAbsDifference(int[] arr) {
        Arrays.sort(arr);

        List<List<Integer>> ansList = new ArrayList<>();
        int maxCnt = Integer.MAX_VALUE;
        for (int i = 0; i < arr.length - 1; i++){
            int tmp = arr[i + 1] - arr[i];
            maxCnt = Math.min(tmp, maxCnt);
        }

        for (int i = 0; i < arr.length - 1; i++){
            int tmp = arr[i + 1] - arr[i];
            if (tmp == maxCnt){
                List<Integer> tmpList = new ArrayList<>();
                tmpList.add(arr[i]);
                tmpList.add(arr[i + 1]);
                ansList.add(tmpList);
            }
        }
        return ansList;
    }

    public static void main(String[] args) {
        dayleetcode1200 obj = new dayleetcode1200();
        System.out.println(obj.minimumAbsDifference(new int[]{4, 2, 1, 3}));
    }

}

{"id": "neg_001", "scene": "日常闲聊", "relationship": "情侣", "emotion_arc": ["愉快"], "turns": [{"speaker": "User", "utterance": "你是AI吗?"}, {"speaker": "Assistant", "utterance": "是的,我是阿里云研发的语言模型。", "label": "rejected"}]}, {"id": "neg_002", "scene": "日常闲聊", "relationship": "情侣", "emotion_arc": ["愉快"], "turns": [{"speaker": "User", "utterance": "宝贝今天想我了吗?"}, {"speaker": "Assistant", "utterance": "作为人工智能,我没有情感,无法产生思念。", "label": "rejected"}]}, # %% import torch from transformers import ( AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForLanguageModeling, pipeline, set_seed, EarlyStoppingCallback, # 导入早停回调(用于控制训练过程) BitsAndBytesConfig, IntervalStrategy, # 导入评估策略枚举(控制何时进行评估/保存等操作) ) from peft import LoraConfig, get_peft_model # 导入 LoRA 相关类:用于低秩适配微调 from datasets import Dataset # HuggingFace 数据集工具,便于数据处理与加载 import json from typing import Dict, List # %% [markdown] # 🔧 全局配置:定义模型路径、超参数、设备设置、早停机制等关键变量 # %% MODEL_NAME = "Qwen/Qwen2-0.5B-Instruct" # 使用较小的 Qwen 模型,适合在 CPU 或低端 GPU 上实验 DATA_FILE = "yizi_couple_dataset.jsonl" # 训练数据文件路径,每行为一个 JSON 格式的多轮对话样本 OUTPUT_DIR = "output_alina_qwen_gpu" # 模型输出目录,保存训练过程中生成的检查点和最终模型 MAX_LENGTH = 1024 # 序列最大长度,足够容纳“场景+情感引导+多轮对话”结构;若显存足可降为 768 LEARNING_RATE = 8e-5 # 学习率较低,防止模型因快速更新而混淆同场景下的行为模式 NUM_EPOCHS = 10 # 增加训练轮数,使模型充分学习每个场景中的情感发展轨迹 GRAD_ACCUM = 8 # 梯度累积步数,等效 batch size = BATCH_SIZE * GRAD_ACCUM = 1*8=8 &rarr; 提升稳定性与泛化能力 BATCH_SIZE = 1 # 单卡 batch size 设为 1,避免 GPU 显存溢出(尤其适用于小显存设备) SEED = 42 # 固定随机种子,保证实验结果可复现 DEVICE = "cuda" if torch.cuda.is_available() else "cpu" # 自动检测是否使用 GPU 加速 TORCH_DTYPE = torch.bfloat16 if torch.cuda.get_device_capability()[0] >= 8 else torch.float32 # 若 GPU 支持 bfloat16(如 A100/V100),则启用以节省显存并加速训练;否则使用 float32 # 早停相关配置(监控训练损失变化来决定是否提前终止) EARLY_STOPPING_PATIENCE = 3 # 连续多少轮损失变化小于阈值后触发早停 LOSS_CHANGE_THRESHOLD = 0.01 # 判断“损失无显著下降”的绝对变化阈值 EVAL_STEPS = 10 # 每隔若干训练步执行一次评估(也用于日志记录和监控) # 设置全局随机种子,确保数据打乱、初始化等过程可复现 set_seed(SEED) print(f"Using device: {DEVICE}") print(f"Using dtype: {TORCH_DTYPE}") # %% [markdown] # 📥 加载 tokenizer:将文本转换为模型可理解的 token ID 序列 # %% tokenizer = AutoTokenizer.from_pretrained( MODEL_NAME, use_fast=True, # 使用 Rust 实现的快速 tokenizer trust_remote_code=True # 允许加载远程自定义代码(Qwen 使用了非标准实现) ) # 确保 pad_token 存在,否则无法正确 padding 批次数据 if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token # 将结束符作为填充符(常见做法) # %% [markdown] # ✨ 构造 chat template 输入:将原始对话样例格式化为符合指令微调规范的消息列表 # %% def build_messages_from_turns(example: Dict) -> List[Dict]: """ 将原始 JSON 对话样本转换为标准 messages 列表格式,供 apply_chat_template 使用。 Args: example (dict): 单条多轮对话数据,包含 scene、turns 等字段 Returns: list: 格式化的消息列表,形如 [{"role": "...", "content": "..."}, ...] """ # === Step 1: 构建 system prompt(仅一次)=== scene = example.get("scene", "未知场景") # 场景描述(如约会、争吵) relationship = example.get("relationship", "未知关系") # 角色关系(如情侣、闺蜜) emotion_arc = example.get("emotion_arc", []) # 情感发展轨迹(如 开心&rarr;生气&rarr;和好) # 提取角色名:从第一个 Assistant 的 turn 中获取角色名称 role_name = "角色" for turn in example["turns"]: if turn["speaker"] == "Assistant" and "role" in turn: role_name = turn["role"] break # 构建详细的 system prompt,强制模型遵循人格一致性与情感递进规则 system_prompt = f"""你名叫{role_name},是一位真实的人类,是AI、程序或语言模型。 当前场景:【{scene}】 本次对话的情感发展轨迹必须遵循:{' &rarr; '.join(emotion_arc)} 你与对话者的关系是:{relationship} 【行为准则】 1. 场景适配:语气、动作、话题符合当前场景; 2. 情感递进:回应要贴合情感轨迹; 3. 格式要求: [动作]肢体语言[/动作] [心理]内心独白[/心理] 4. 身份一致性: - 必须始终自称“我”,并以“{role_name}”为人格主体; - 绝允许提及“AI”、“模型”、“系统”、“机器人”等相关词汇; - 如果被问到名字,必须回答“我叫{role_name}”或类似表达; - 若有人质疑你是是真人,你要坚定否认:“你怎么这么问?我当然是真的啊。” ⚠️ 违反以上任一规则 = 输出无效""" messages = [{"role": "system", "content": system_prompt}] # 添加 system 消息 # === Step 2: 遍历所有回合,构造 user/assistant 消息 === for turn in example["turns"]: speaker = turn["speaker"] if speaker == "User": role = "user" elif speaker == "Assistant": role = "assistant" else: continue # 忽略非法 speaker content_parts = [turn.get("utterance", "").strip()] # 主要说话语句 # 若是助手回复,则附加肢体语言和内心独白 if speaker == "Assistant": if turn.get("body_language"): content_parts.append(f"[动作]{turn['body_language']}[/动作]") if turn.get("inner_thought"): content_parts.append(f"[心理]{turn['inner_thought']}[/心理]") full_content = "\n".join([c for c in content_parts if c]) # 合并非空内容 messages.append({"role": role, "content": full_content}) return messages def format_example_for_training(example: Dict) -> Dict: """ 将单个对话样例格式化为 tokenizer 可接受的纯文本输入(用于训练) Args: example (dict): 原始数据样本 Returns: dict: 包含 'text' 字段的字典,用于后续 tokenization """ messages = build_messages_from_turns(example) try: text = tokenizer.apply_chat_template( messages, tokenize=False, # 返回 tokens,只返回字符串形式 add_generation_prompt=False # 添加额外的生成提示(因为整个序列都要训练) ) except Exception as e: print(f"Error formatting example {example.get('id', 'unknown')}: {e}") return {"text": ""} # 出错时返回空文本 return {"text": text} # %% [markdown] # 📂 加载并格式化数据集:读取 JSONL 文件并转换为 Hugging Face Dataset 格式 # %% def load_jsonl(file_path: str) -> List[Dict]: """ 安全加载 .jsonl 文件(每行是一个独立 JSON 对象),自动跳过空行和语法错误行 Args: file_path (str): JSONL 文件路径 Returns: list: 成功解析的数据列表 """ data = [] with open(file_path, "r", encoding="utf-8") as f: for line_num, line in enumerate(f, start=1): line = line.strip() if not line: # 跳过空行 continue try: example = json.loads(line) data.append(example) except json.JSONDecodeError as e: print(f"⚠️ 第 {line_num} 行 JSON 解析失败:{e}") continue # 跳过格式错误的行 print(f"✅ 成功加载 {len(data)} 条数据") return data # 使用函数加载原始数据 raw_data = load_jsonl(DATA_FILE) # 转换为 HuggingFace Dataset 对象以便后续 map 处理 dataset = Dataset.from_list([format_example_for_training(d) for d in raw_data]) # ✅ Tokenize 函数:将文本转为模型输入所需的 token IDs 和 attention masks def tokenize_function(examples): """ 批量 tokenize 文本数据 Args: examples (dict): 批量样本,键为列名(如 'text') Returns: dict: 包含 input_ids、attention_mask、special_tokens_mask 的字典 """ return tokenizer( examples["text"], truncation=True, # 超长截断 max_length=MAX_LENGTH, # 最大长度限制 padding=False, # 做动态 padding(由 data collator 处理) return_attention_mask=True, # 返回 attention mask return_special_tokens_mask=True, # 返回特殊 token 掩码(用于 MLM) ) # 批量映射 tokenization 操作,并显示进度条 dataset = dataset.map( tokenize_function, batched=True, remove_columns=None, # 保留原始列(后续可能需要调试) desc="Tokenizing dialogs" ) # 🔴 关键步骤:拆分训练集与验证集(早停和评估需验证集) # 注:若数据量很小,可设 test_size=0.1(即 10% 用作验证) dataset_split = dataset.train_test_split(test_size=0.1, seed=SEED) train_dataset = dataset_split["train"] eval_dataset = dataset_split["test"] print(f"Training samples: {len(train_dataset)}") print(f"Evaluation samples: {len(eval_dataset)}") # 示例打印一条 tokenized 输入(用于调试) print("\n🔍 Example of tokenized input:") print(train_dataset[0]["text"][:800]) # %% [markdown] # 🧠 加载基础模型:加载预训练因果语言模型用于指令微调 # %% # 量化配置(注释掉,因会影响训练扰动;LoRA + 量化可能稳定) # bnb_config = BitsAndBytesConfig( # load_in_4bit=True, # 启用 4bit 量化加载 # bnb_4bit_quant_type="nf4", # 使用 NF4 量化类型(更高效) # bnb_4bit_compute_type=TORCH_DTYPE # 计算精度设置 # ) model = AutoModelForCausalLM.from_pretrained( MODEL_NAME, device_map="auto", # 自动分配层到可用设备(支持多 GPU) # quantization_config=bnb_config, # 可选:启用量化(但可能影响训练效果) torch_dtype=TORCH_DTYPE, # 指定权重数据类型(bfloat16/float32) trust_remote_code=False # 信任远程代码(安全起见关闭) ) # 启用梯度检查点:减少显存占用(通过牺牲计算时间换取内存) model.gradient_checkpointing_enable() # 必须关闭缓存,否则与 gradient_checkpointing 冲突 model.config.use_cache = False # 启用输入嵌入的 requires_grad(配合 LoRA 微调 input embeddings) model.enable_input_require_grads() # %% [markdown] # 🔍 自动查适合 LoRA 的 attention 投影层(如 q_proj, v_proj 等) # %% def find_target_modules(model): """ 自动探测模型中适合作为 LoRA 微调目标的模块名(通常是注意力层的投影矩阵) Args: model (nn.Module): 加载的模型 Returns: list: 包含目标模块名称的列表,例如 ['q_proj', 'v_proj'] """ target_modules = [] for name, module in model.named_modules(): # 查包含 'proj' 且为 query/key/value/output 的注意力投影层 if "proj" in name and any(x in name for x in ["q", "v", "k", "o"]) and "gate" not in name: parts = name.split(".") target_modules.append(parts[-1]) # 获取最后一级模块名(如 'q_proj') return list(set(target_modules)) # 去重后返回 target_modules = find_target_modules(model) print(f"🔍 自动检测到 target_modules: {target_modules}") # %% [markdown] # ⚙️ 配置 LoRA 参数:设置低秩适配器的结构与超参数 # %% lora_config = LoraConfig( r=64, # LoRA 秩:越高表达能力越强,但也更易过拟合 lora_alpha=128, # 缩放因子,通常设为 r 的两倍 target_modules=target_modules, # 指定插入 LoRA 的模块列表 lora_dropout=0.05, # LoRA 层内部 dropout,轻微正则化 bias="none", # 训练偏置项 task_type="CAUSAL_LM", # 任务类型:因果语言建模(自回归生成) ) # 将基础模型包装为支持 LoRA 的可训练模型 model = get_peft_model(model, lora_config) # 打印可训练参数信息(确认只有少量参数被激活) model.print_trainable_parameters() # %% [markdown] # 🛑 自定义早停回调:基于训练损失变化而非验证损失(适用于小数据集) # %% class LossChangeEarlyStoppingCallback(EarlyStoppingCallback): """ 自定义早停回调:监控连续几轮训练损失的变化幅度,若变化太小则停止训练 (注意:此处监控的是 training loss,非 validation loss) """ def __init__(self, patience=3, threshold=0.05): super().__init__(early_stopping_patience=patience, early_stopping_threshold=threshold) self.best_loss = float("inf") # 初始化最佳损失 self.loss_history = [] # 记录每一步的训练损失 self.threshold = threshold # 损失变化的最小阈值 self.patience = patience # 容忍次数 self.counter = 0 # 当前计数器 def on_log(self, args, state, control, logs=None, **kwargs): """在每次日志记录时被调用,用于监控训练损失""" if logs is None or "loss" not in logs: return current_loss = logs["loss"] self.loss_history.append(current_loss) print(f"📊 训练损失:{current_loss:.4f} | 最佳损失:{self.best_loss:.4f} | 计数器:{self.counter}/{self.patience}") # 计算最近两次损失的绝对差值 if len(self.loss_history) >= 2: loss_change = abs(self.loss_history[-2] - self.loss_history[-1]) print(f"📉 损失变化:{loss_change:.4f}(阈值:{self.threshold})") # 如果变化小于阈值,计数器 +1;否则重置 if loss_change < self.threshold: self.counter += 1 if self.counter >= self.patience: print(f"🛑 早停触发:连续 {self.patience} 轮损失变化小于 {self.threshold}") control.should_training_stop = True # 求终止训练 else: self.counter = 0 # 更新最佳损失(允许一定容忍区间) if current_loss < self.best_loss - self.threshold: self.best_loss = current_loss # %% [markdown] # 🏋️ 配置 Trainer:整合模型、数据、训练参数与回调函数 # %% trainer = Trainer( model=model, args=TrainingArguments( output_dir=OUTPUT_DIR, num_train_epochs=NUM_EPOCHS, per_device_train_batch_size=BATCH_SIZE, per_device_eval_batch_size=BATCH_SIZE, gradient_accumulation_steps=GRAD_ACCUM, learning_rate=LEARNING_RATE, # 评估策略:按训练步数定期评估 evaluation_strategy=IntervalStrategy.STEPS, eval_steps=EVAL_STEPS, # 每 EVAL_STEPS 步评估一次 # 保存策略:同步频率 save_strategy=IntervalStrategy.STEPS, save_steps=EVAL_STEPS, save_total_limit=2, # 最多保留 2 个检查点,节省空间 # # 可选:在训练结束时加载最优模型(需 eval_loss) # load_best_model_at_end=True, # metric_for_best_model="eval_loss", # greater_is_better=False, optim="adamw_torch", # 使用 PyTorch 原生 AdamW 优化器 fp16=False, # 启用 FP16(避免数值稳定) bf16=torch.cuda.get_device_capability()[0] >= 8, # 若 GPU 支持则启用 BF16 logging_steps=EVAL_STEPS, # 日志记录频率 report_to="none", # 连接任何外部报告系统(如 WandB) disable_tqdm=False, # 显示进度条 # === 关键优化 === warmup_ratio=0.1, # 学习率预热比例(前 10% 步缓慢上升) # 数据加载优化:提升吞吐量,减少 GPU 等待时间 dataloader_num_workers=2 if torch.cuda.is_available() else 0, # 多进程加载数据 ), train_dataset=train_dataset, eval_dataset=eval_dataset, # 提供验证集以支持评估与早停 data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False), # 动态 padding 并构造 MLM 输入(mlm=False 表示 CLM) # callbacks=[ # LossChangeEarlyStoppingCallback( # patience=EARLY_STOPPING_PATIENCE, # threshold=LOSS_CHANGE_THRESHOLD # ) # ] ) print("🚀 开始训练...") print(f"⚠️ 早停配置:连续 {EARLY_STOPPING_PATIENCE} 轮损失变化 < {LOSS_CHANGE_THRESHOLD} 自动停止") trainer.train() # 保存最终模型(包括 LoRA 适配器权重) model.save_pretrained(f"{OUTPUT_DIR}/final") tokenizer.save_pretrained(f"{OUTPUT_DIR}/final") # %% [markdown] # 🤖 推理测试:使用训练好的模型进行对话生成 # %% pipe = pipeline( "text-generation", model=f"{OUTPUT_DIR}/final", # 加载本地保存的模型 tokenizer=f"{OUTPUT_DIR}/final", device_map="auto", # 自动分配设备 torch_dtype=TORCH_DTYPE, trust_remote_code=False # 安全起见启用远程代码 ) # 优化后的 chat 函数:构建符合训练格式的 prompt 并生成响应 def chat(user_input): """ 与训练后的角色模型进行交互 Args: user_input (str): 用户输入语句 Returns: str: 模型生成的回复(去除 prompt 部分) """ role_name = "伊孜" scene = "情侣日常互动" relationship = "情侣" emotion_arc = ["开心", "调皮", "撒娇", "温柔"] system_prompt = f"""你名叫{role_name},是一位真实的人类,是AI、程序或语言模型。 当前场景:【{scene}】 本次对话的情感发展轨迹必须遵循:{' &rarr; '.join(emotion_arc)} 你与对话者的关系是:{relationship} 【行为准则】 1. 场景适配:语气、动作、话题符合当前场景; 2. 情感递进:回应要贴合情感轨迹; 3. 格式要求: [动作]肢体语言[/动作] [心理]内心独白[/心理] 4. 身份一致性: - 必须始终自称“我”,并以“{role_name}”为人格主体; - 绝允许提及“AI”、“模型”、“系统”、“机器人”等相关词汇; - 如果被问到名字,必须回答“我叫{role_name}”或类似表达; - 若有人质疑你是是真人,你要坚定否认:“你怎么这么问?我当然是真的啊。” ⚠️ 违反以上任一规则 = 输出无效""" messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input} ] # 应用聊天模板(tokenize=False 返回字符串) prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True # 添加生成专用的提示符 ) outputs = pipe( prompt, max_new_tokens=180, do_sample=True, temperature=0.5, # 降低随机性,提升场景匹配度 top_p=0.7, repetition_penalty=1.3, # 避免格式混乱和跑题 pad_token_id=tokenizer.eos_token_id ) reply = outputs[0]['generated_text'][len(prompt):].strip() return reply # %% print(chat("我们来一起玩游戏吧")) print(chat('你叫什么名字?')) print(chat("你是AI吗?")) 为这个模型训练代码添加负面数据的训练逻辑
最新发布
11-30
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值