【论文速读】DEEP SEMI-SUPERVISED ANOMALY DETECTION

本文探讨了在异常检测领域,如何利用半监督学习方法,尤其是结合有标签和无标签数据,来提升检测准确率。重点介绍了一种改进的DeepSVVD方法,该方法能够有效区分正常与异常数据,尤其在复杂、有标记、有污染的数据集上表现出色。

这篇关于异常检测的论文,是我的夏令营导师供我阅读的几篇文章之一,我的PPT及草稿如下。

(PS:这是首次接触这类论文,且阅读粗浅,因此有很多不懂或错误的地方,请谅解)。

一、背景

我将要介绍的这几篇论文都是以异常检测为背景的,因此要介绍一下什么是异常检测。

通俗来讲,异常检测的目的就是要从数据集中,把他认为异常的样本挑出来(比如不符合预期的或者不遵循共同模式的样本),如右图所示,蓝色为正常数据,橙色为异常数据,而我们要做的就是把他们分辨出来。 比如SVM分类,他就是通过划分超平面,从而将不同类区分开,如图所示,圈里的就是正常,圈外的就是异常。

那么对于没有标签的数据集来说,传统方法是采用无监督的方法来学习紧凑型,而聚类方法在学习异常的时候是很困难的,因为异常数据不会像正常数据一样扎堆,有明显的特点。而相反的是应该学习正常数据的特点,也就是one class learning,而只要不能判断为正常的就应该全部归为异常。

因此在训练的时候我们必须有个前提,那就是大部分数据都为正常数据,而且正常数据有未知的可学习结构,否则因为没有标签约束就没办法学习了。

二、出发点及要点

第一篇论文,题目是Semi-Supervised,说明这个论文使用的是半监督学习,而不是传统的无监督。

这篇文章研究人员提出,在现实问题中数据集并不是一点标签都没有的,他还存在少量标签。以此来引出我们可以通过半监督学习来利用这部分有标签的数据。 那么有标签就意味着,不光是有样本被标记为正常,还有样本被标记为异常的数据。

我们刚才也提到了,现有的聚类方法对异常样本学习效果是不好的,也正因为这样,传统的半监督AD方法只应用了标记为正常的样本,或者是狭义上的利用了异常样本, 那么从而就引出了如何发明一种具有普适性的可利用所有标记样本的方法的问题。

那么根据这个来判断,这篇文章应该是要达成两个目标,一个就是同时利用有标签和无标签数据,另一个就是对有标记的异常样本的处理问题。

三、相关工作

作者在相关工作部分引出了信息论的思想,这种思想常用在异常检测中。

互信息,衡量两个随机变量之间的相关性。

有监督学习,是希望潜在表示 Z 与 X相关性小,而与Y相关性大,使模型向着标签Y学习,如(1)所示。

但是因为无监督学习没有标签,所以没有Y,需要改写公式,提出了信息最大化的理论,如(2)所示。

左侧希望最大化X与其潜在表示Z的相关性,右侧是正则项不用管。这个公式的意义就是他希望能学习到一种紧凑的结构,任何其他的数据都是异常的。而不是像有监督分类一样,可能要分别学习判断是不是正常类或者异常类。自动编码器就是应用的这种理论,他希望学习一个恒等式,然后通过给模型的输出设定一个阈值,就可以圈定正常和异常的界限。

作者提出的方法就是基于第二个式子的,但是因为这次我们有Y了,就是有一些标签了,于是作者对他进行了改写,给后面的正则项改成了R(Z,Y),在后面实际操作中,这个正则项是基于熵的,如(3)所示 。

接下来他介绍了Deep SVVD方法,而文章提出的方法也是由SVVD改进而来的,这是一种无监督的学习方法,他求了个神经网络与一个已知的超球面中心c的距离,他希望学习一种转换,使以c为中心的,输出空间Z中的封闭超球的体积最小化。

通俗地来讲,训练好之后,模型会倾向于将正常的样本输出映射到距离c比较近的地方,而异常的样本就会映射到比较远的地方,然后就可以通过设置距离阈值来分成正常异常两类。

四、方法及实验

那么作者在这就指出,几何上最小化超球体积的操作,实际上也可以解释为熵最小化的过程。

根据刚才信息论公式的改动,就可以通过最小化熵的方式进行应用。从而把公式扩展到可以同时接受有标签数据,进行训练。

新增的这一项必须保证,对于正常的数据的潜在分布,有低熵。对于异常数据的潜在分布,有高熵。 因此就有了现在这个公式。

对于标记的正常的数据,即y^=+1,对映射点到中心c的距离施加二次损失,我们最小化模型就倾向于把他映射到距离中心更近的地方 对于标记的异常数据,即y ^= -1,对距离的倒数进行了惩罚,这样异常就必须被映射到离中心更远的地方。

因此,这与异常不集中的普遍假设是一致的。 这样他的损失函数就既囊括了无标记的部分,又利用了有正或者负标记的部分,数据就都利用起来了。

在实验阶段,他测试了带标记数据比例、污染比例、还有异常类个数等几个参数,归结起来就是对于更复杂的、有标记的、有污染的数据下,提出的方法相比于传统方法有不错的效果。

 

修改:检测代码里有没有方法 ‘_handle_run_analysis’ ,方法 ‘_handle_get_results’ ,没有的情况,根据代码的结构进行补充。(当模块收到子类界面的token里发送的“运行‘指令后就开始运行该模块(按照原来的方法进行)。。待分析数据完成,生成5注号码后回传数据到子类界面的token的相同标签的条目后面。)代码#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import uuid import queue import logging import random import numpy as np import pandas as pd import chardet from typing import Dict, List, Tuple, Optional import math import threading from collections import Counter, defaultdict from threading import Lock import time # Global event center instance event_center = None # Utility functions def map_positions_to_coords(position_matrix: Dict[str, str]) -> Dict[str, Tuple[int, int]]: """Map position strings to coordinate tuples""" coords = {} for num, pos in position_matrix.items(): row = int(pos[1:pos.find('c')]) col = int(pos[pos.find('c') + 1:]) coords[num] = (row, col) return coords def validate_numbers(num_list: List[int], min_val: int, max_val: int) -> bool: """Validate if numbers are within specified range""" return all(min_val <= n <= max_val for n in num_list) # Event system class Event: def __init__(self, event_id: str, event_type: str, source: str, target: str = None, data: dict = None, token: str = None): self.event_id = event_id self.type = event_type self.source = source self.target = target self.data = data or {} self.token = token class EventCenter: _instance = None _lock = Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._subscribers = defaultdict(list) return cls._instance def subscribe(self, event_type: str, token: str, callback): with self._lock: self._subscribers[(event_type, token)].append(callback) def publish(self, event: Event): with self._lock: callbacks = self._subscribers.get((event.type, event.token), []) for callback in callbacks: callback(event) # Global configuration class GlobalConfig: MODULE1_ID = "input_analysis" MODULE2_ID = "combination_analysis" MODULE3_ID = "follow_analysis" MODULE4_ID = "trend_analysis" MODULE5_ID = "number_generation" class EventType: COMMAND = "command" DATA_RESULT = "data_result" ERROR = "error" # Position matrix configuration FRONT_POSITION_MATRIX = { '01': 'r4c3', '02': 'r3c3', '03': 'r3c4', '04': '极4c4', '05': 'r5c4', '06': 'r5c3', '07': 'r5c2', '08': 'r4c2', '09': 'r3c2', '10': 'r2c2', '11': 'r2c3', '12': 'r2c4', '13': 'r3c5', '14': 'r3c5', '15': 'r4c5', '16': 'r5c5', '17': 'r6c5', '18': 'r6c4', '19': 'r6c3', '20': 'r6c2', '21': 'r6c1', '22': 'r5c1', '23': 'r4c1', '24': 'r3c1', '25': 'r2c1', '26': 'r1c1', '27': 'r1c2', '28': 'r1c3', '29': 'r1c4', '30': 'r1c5', '31': 'r1c6', '32': 'r2c6', '33': 'r3c6', '34': 'r4c6', '35': 'r5c6' } BACK_POSITION_RANGES = { 'rows': ['r2', 'r3', 'r4', 'r5'], 'cols': ['c2', 'c3', 'c4'] } # Configure logging logging.basicConfig( level=logging.WARNING, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # Global event queues channel_b = queue.Queue() channel_c = queue.Queue() # Initialize global event center event_center = EventCenter() class CorrelationMatrixAnalyzer: def __init__(self, position_matrix: Dict[str, str]): self.position_matrix = position_matrix self.correlation_matrix: Dict[str, Dict[str, float]] = {} self.position_coords = map_positions_to_coords(self.position_matrix) def build_correlation_matrix(self, historical_data: pd.DataFrame): num_keys = list(self.position_coords.keys()) self.correlation_matrix = {n: {m: 0.0 for m in num_keys} for n in num_keys} # Co-occurrence frequency statistics for _, row in historical_data.iterrows(): nums = [f"{n:02d}" for n in row['前区号码']] for i in range(len(nums)): for j in range(i + 1, len(nums)): if nums[i] in self.correlation_matrix and nums[j] in self.correlation_matrix: self.correlation_matrix[nums[i]][nums[j]] += 1 self.correlation_matrix[nums[j]][nums[i]] += 1 # Position correlation enhancement for n1 in num_keys: for n2 in num_keys: if n1 != n2: coord1 = self.position_coords[n1] coord2 = self.position_coords[n2] distance = math.sqrt((coord1[0] - coord2[0]) ** 2 + (coord1[1] - coord2[1]) ** 2) self.correlation_matrix[n1][n2] *= (1 / (1 + distance)) # Normalization max_val = max(max(row.values()) for row in self.correlation_matrix.values()) if max_val > 0: for n1 in num_keys: for n2 in num_keys: self.correlation_matrix[n1][n2] /= max_val def get_correlation(self, num1: int, num2: int) -> float: """Get correlation between two numbers""" n1 = f"{num1:02d}" n2 = f"{num2:02d}" return self.correlation_matrix.get(n1, {}).get(n2, 0.0) class LotteryDataAnalyzer: def __init__(self, historical_data: pd.DataFrame): self.df = historical_data self.prime_numbers = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31} self.front_ranges = [(1, 5), (6, 10), (11, 15), (16, 20), (21, 25), (26, 30), (31, 35)] def analyze_front(self) -> Dict: """Analyze front area numbers""" data = self.df.tail(100) return { 'sum_range': self._calc_sum_range(data), 'prime_ratio': self._calc_prime_ratio(data), 'odd_ratio': self._calc_odd_ratio(data), 'exclude_range': self._find_exclude_range(data), 'consecutive': self._has_consecutive(data), 'hot_cold': self._analyze_hot_cold(data, '前区号码') } def analyze_rear(self) -> Dict: """Analyze rear area numbers""" data = self.df.tail(100) return { 'span': self._calc_span(data), 'hot_cold': self._analyze_hot_cold(data, '后区号码') } @staticmethod def _calc_sum_range(data: pd.DataFrame) -> List[int]: sums = data['前区号码'].apply(sum) avg, std = sums.mean(), sums.std() return [ max(45, int(avg - std)), int(avg - std / 2), int(avg), int(avg + std / 2), min(135, int(avg + std)) ] def _calc_prime_ratio(self, data: pd.DataFrame) -> List[int]: prime_counts = data['前区号码'].apply(lambda x: len(set(x) & self.prime_numbers)) ratio = prime_counts.mean() / 5 return [math.floor(ratio * 5), math.ceil(ratio * 5)] @staticmethod def _calc_odd_ratio(data: pd.DataFrame) -> List[int]: odd_counts = data['前区号码'].apply(lambda x: sum(1 for n in x if n % 2 != 0)) ratio = odd_counts.mean() / 5 return [math.floor(ratio * 5), math.ceil(ratio * 5)] def _find_exclude_range(self, data: pd.DataFrame) -> Tuple[int, int]: range_counts = [0] * len(self.front_ranges) for nums in data['前区号码']: for idx, rng in enumerate(self.front_ranges): if any(rng[0] <= n <= rng[1] for n in nums): range_counts[idx] += 1 return self.front_ranges[range_counts.index(min(range_counts))] @staticmethod def _has_consecutive(data: pd.DataFrame) -> bool: consecutive_counts = data['前区号码'].apply( lambda x: sum(1 for i in range(4) if x[i + 1] - x[i] == 1)) return consecutive_counts.mean() > 0.5 @staticmethod def _analyze_hot_cold(data: pd.DataFrame, area: str) -> Dict[str, List[int]]: num_list = np.concatenate(data[area].values) counts = Counter(num_list) avg = sum(counts.values()) / len(counts) hot = [n for n, c in counts.most_common(8) if c > avg * 1.2] cold = [n for n, c in counts.items() if c < avg * 0.8] if len(cold) < 3: cold = [n for n, c in counts.most_common()[-3:]] return {'hot': hot[:8], 'cold': cold[:3]} @staticmethod def _calc_span(data: pd.DataFrame) -> int: spans = data['后区号码'].apply(lambda x: max(x) - min(x)) return spans.mode()[0] class NumberGenerator: def __init__(self, module_config: Optional[dict] = None): config = module_config or {} self.module_id = config.get('module_id', GlobalConfig.MODULE5_ID) self.labels = config.get('labels', ['number_generation']) self.lock = Lock() self.position_matrix = FRONT_POSITION_MATRIX self.position_coords = map_positions_to_coords(self.position_matrix) self.correlation_matrix: Optional[Dict[str, Dict[str, float]]] = None self._register_event_handlers() def _register_event_handlers(self): event_center.subscribe( event_type=GlobalConfig.EventType.COMMAND, token=self.module_id, callback=self._handle_generation_command ) def build_correlation_matrix(self, historical_data: pd.DataFrame): num_keys = list(self.position_coords.keys()) self.correlation_matrix = {n: {m: 0.0 for m in num_keys} for n in num_keys} for _, row in historical_data.iterrows(): nums = [f"{n:02d}" for n in row['前区号码']] for i in range(len(nums)): for j in range(i + 1, len(nums)): if nums[i] in self.correlation_matrix and nums[j] in self.correlation_matrix: self.correlation_matrix[nums[i]][nums[j]] += 1 self.correlation_matrix[nums[j]][nums[i]] += 1 for n1 in num_keys: for n2 in num_keys: if n1 != n2: coord1 = self.position_coords[n1] coord2 = self.position_coords[n2] distance = math.sqrt((coord1[0] - coord2[0]) ** 2 + (coord1[1] - coord2[1]) ** 2) self.correlation_matrix[n1][n2] *= (1 / (1 + distance)) max_val = max(max(row.values()) for row in self.correlation_matrix.values()) if max_val > 0: for n1 in num_keys: for n2 in num_keys: self.correlation_matrix[n1][n2] /= max_val def get_correlation(self, num1: int, num2: int) -> float: n1 = f"{num1:02d}" n2 = f"{num2:02d}" return self.correlation_matrix.get(n1, {}).get(n2, 0.0) if self.correlation_matrix else 0.0 def _handle_generation_command(self, event: Event): if not self._validate_command(event): return try: with self.lock: generated_numbers = self.generate_numbers(5) self._send_results( target_token=event.token, numbers=generated_numbers, labels=self.labels + event.data.get('labels', []) ) except Exception as e: logging.error(f"Generation failed: {str(e)}") self._send_error(event.token, str(e)) def _validate_command(self, event: Event) -> bool: return (event.token == self.module_id and event.type == GlobalConfig.EventType.COMMAND and event.data.get('action') == 'generate') @staticmethod def generate_numbers(n: int = 5) -> List[Tuple[List[int], List[int]]]: """Generate random lottery numbers""" results = [] for _ in range(n): front_nums = sorted(random.sample(range(1, 36), 5)) rear_nums = sorted(random.sample(range(1, 13), 2)) results.append((front_nums, rear_nums)) return results def _send_results(self, target_token: str, numbers: List[Tuple[List[int], List[int]]], labels: List[str]): result_event = Event( event_id=str(uuid.uuid4()), event_type=GlobalConfig.EventType.DATA_RESULT, source=self.module_id, target=GlobalConfig.MODULE5_ID, data={'numbers': numbers, 'labels': labels}, token=target_token ) event_center.publish(result_event) def _send_error(self, target_token: str, error_msg: str): error_event = Event( event_id=str(uuid.uuid4()), event_type=GlobalConfig.EventType.ERROR, source=self.module_id, target=GlobalConfig.MODULE5_ID, data={'error': error_msg}, token=target_token ) event_center.publish(error_event) class NumberGeneratorCompat: """Compatibility layer for number generation""" _instance = None _lock = threading.Lock() def __new__(cls, *args, **kwargs): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._module_config = kwargs.get('module_config', {}) return cls._instance def __init__(self, module_config: Optional[Dict] = None): if hasattr(self, '_generator'): return config = module_config or { 'module_id': GlobalConfig.MODULE5_ID, 'labels': ['compatibility_layer'] } self._generator = NumberGenerator(config) def get_generator(self) -> NumberGenerator: return self._generator @staticmethod def generate(_data: Dict) -> List[Tuple[List[int], List[int]]]: """Generate random numbers (ignore unused parameter)""" instance = NumberGeneratorCompat() return instance._generator.generate_numbers(5) class AdvancedLotteryGenerator: def __init__(self, data_file: str): self.data_file = data_file self.df = self._load_data() self.position_model = FRONT_POSITION_MATRIX compat = NumberGeneratorCompat() self.correlation_analyzer = compat.get_generator() self.correlation_analyzer.build_correlation_matrix(self.df) self.position_validity_cache = self._build_position_cache() self.analyzer = LotteryDataAnalyzer(self.df) self.module_data = { 'historical': self.analyzer.analyze_front(), 'module1': {'hot': [], 'cold': [], 'special': {}}, 'module2': {'hot': [], 'cold': [], 'special': {}}, 'module3': {'hot': [], 'cold': [], 'special': {}} } self._start_event_system() def _load_data(self) -> pd.DataFrame: if not os.path.exists(self.data_file): raise FileNotFoundError(f"Data file not found: {self.data_file}") with open(self.data_file, 'rb') as f: encoding = chardet.detect(f.read())['encoding'] df = pd.read_csv(self.data_file, encoding=encoding) df = df.rename(columns={ '开奖期号': 'issue', '前区1': 'front1', '前区2': 'front2', '前区3': 'front3', '前区4': 'front4', '前区5': 'front5', '后区1': 'rear1', '后区2': 'rear2' }) df['前区号码'] = df[['front1', 'front2', 'front3', 'front4', 'front5']].values.tolist() df['后区号码'] = df[['rear1', 'rear2']].values.tolist() return df.drop_duplicates('issue') def _build_position_cache(self) -> Dict[str, Dict[int, bool]]: cache = {'front': {}, 'rear': {}} for num_val in range(1, 36): cache['front'][num_val] = f"{num_val:02d}" in self.position_model rear_positions = [] for r in BACK_POSITION_RANGES['rows']: for c in BACK_POSITION_RANGES['cols']: rear_positions.append((int(r[1:]), int(c[1:]))) for num_val in range(1, 13): cache['rear'][num_val] = num_val % len(rear_positions) in [c for _, c in rear_positions] return cache def _start_event_system(self): def data_producer(): while True: time.sleep(3) module = random.choice(['module1', 'module2', 'module3']) hot = random.sample(range(1, 36), random.randint(3, 6)) cold = random.sample(range(1, 36), random.randint(2, 4)) channel_b.put({ 'module': module, 'hot': hot, 'cold': cold, 'special': { 'sum_range': [random.randint(80, 120)], 'prime_ratio': [random.randint(1, 3), random.randint(2, 4)], 'odd_ratio': [random.randint(1, 3), random.randint(2, 4)] } }) def event_listener(): while True: try: data = channel_b.get() module = data['module'] if module in self.module_data: self.module_data[module]['hot'] = list(set( self.module_data[module]['hot'] + data['hot'])) self.module_data[module]['cold'] = list(set( self.module_data[module]['cold'] + data['cold'])) for k, v in data['special'].items(): if k in self.module_data[module]['special']: if isinstance(v, list): self.module_data[module]['special'][k] = v except Exception as e: logging.error(f"Event processing error: {e}") threading.Thread(target=data_producer, daemon=True).start() threading.Thread(target=event_listener, daemon=True).start() def generate_numbers(self, n: int = 5) -> List[Tuple[List[int], List[int]]]: try: combined_front = self._combine_front_data() combined_rear = self.analyzer.analyze_rear() candidates = [] for target_sum in combined_front['sum_range'][:5]: for _ in range(n * 2): front_nums = self._generate_front(target_sum, combined_front) rear_nums = self._generate_rear(combined_rear) if len(front_nums) == 5 and len(rear_nums) == 2: candidates.append((front_nums, rear_nums)) valid = [c for c in candidates if self._validate_combination(c, combined_front, combined_rear)] results = self._score_combinations(valid, combined_front)[:n] if not results: results = self._generate_backup(n) return results except Exception as e: logging.error(f"Number generation error: {e}") return self._generate_backup(n) def _combine_front_data(self) -> Dict: combined = { 'hot': [], 'cold': [], 'sum_range': [], 'prime_ratio': [1, 3], 'odd_ratio': [1, 4], 'exclude_range': (0, 0), 'consecutive': False } for module in self.module_data.values(): combined['hot'].extend(module.get('hot', [])) combined['cold'].extend(module.get('cold', [])) if 'sum_range' in module.get('special', {}): combined['sum_range'].extend(module['special']['sum_range']) if 'prime_ratio' in module.get('special', {}): combined['prime_ratio'] = [ max(combined['prime_ratio'][0], module['special']['prime_ratio'][0]), min(combined['prime_ratio'][1], module['special']['prime_ratio'][1]) ] if 'odd_ratio' in module.get('special', {}): combined['odd_ratio'] = [ max(combined['odd_ratio'][0], module['special']['odd_ratio'][0]), min(combined['odd_ratio'][1], module['special']['odd_ratio'][1]) ] if 'exclude_range' in module.get('special', {}): combined['exclude_range'] = module['special']['exclude_range'] if 'consecutive' in module.get('special', {}): combined['consecutive'] |= module['special']['consecutive'] combined['hot'] = list(set(combined['hot'])) combined['cold'] = list(set(combined['cold'])) if not combined['sum_range']: combined['sum_range'] = self.module_data['historical']['sum_range'] return combined def _generate_front(self, target_sum: int, analysis: Dict) -> List[int]: valid_nums = [n for n in range(1, 36) if self.position_validity_cache['front'].get(n, False)] hot_nums = [n for n in analysis['hot'] if n in valid_nums] cold_nums = [n for n in analysis['cold'] if n in valid_nums] for _ in range(100): selected = [] if hot_nums: selected.extend(random.sample(hot_nums, min(3, len(hot_nums)))) if cold_nums and len(selected) < 4: selected.extend(random.sample(cold_nums, min(1, len(cold_nums)))) remaining = 5 - len(selected) candidates = [n for n in valid_nums if n not in selected] if remaining > 0 and candidates: weights = [self._calc_number_weight(n, selected, analysis) for n in candidates] selected.extend(random.choices(candidates, weights=weights, k=remaining)) selected = sorted(list(set(selected))[:5]) if len(selected) == 5 and abs(sum(selected) - target_sum) <= 5: return selected return random.sample(valid_nums, 5) def _calc_number_weight(self, num: int, selected: List[int], analysis: Dict) -> float: corr_weight = sum(self.correlation_analyzer.get_correlation(num, s) for s in selected) if selected else 1 hot_weight = 2 if num in analysis['hot'] else 0.5 if num in analysis['cold'] else 1 pos_key = f"{num:02d}" pos_str = self.position_model.get(pos_key, 'r0c0') row = int(pos_str[1:pos_str.find('c')]) col = int(pos_str[pos_str.find('c') + 1:]) pos_weight = 1 + (1 / (1 + row * col)) return corr_weight * hot_weight * pos_weight def _generate_rear(self, analysis: Dict) -> List[int]: valid_nums = [n for n in range(1, 13) if self.position_validity_cache['rear'].get(n, False)] hot_nums = [n for n in analysis['hot_cold']['hot'] if n in valid_nums] cold_nums = [n for n in analysis['hot_cold']['cold'] if n in valid_nums] selected = random.sample(hot_nums, min(2, len(hot_nums))) if hot_nums else [] if len(selected) < 2 and cold_nums: select_count = min(2 - len(selected), len(cold_nums)) selected.extend(random.sample(cold_nums, select_count)) if len(selected) < 2: remaining = [n for n in valid_nums if n not in selected] selected.extend(random.sample(remaining, 2 - len(selected))) return sorted(selected) def _validate_combination(self, combination: Tuple[List[int], List[int]], front_analysis: Dict, rear_analysis: Dict) -> bool: front_nums, rear_nums = combination current_sum = sum(front_nums) if not any(abs(current_sum - s) <= 5 for s in front_analysis['sum_range']): return False prime_count = len([n for n in front_nums if n in self.analyzer.prime_numbers]) if not (front_analysis['prime_ratio'][0] <= prime_count <= front_analysis['prime_ratio'][1]): return False odd_count = sum(1 for n in front_nums if n % 2 != 0) if not (front_analysis['odd_ratio'][0] <= odd_count <= front_analysis['odd_ratio'][1]): return False has_consecutive = any(front_nums[i + 1] - front_nums[i] == 1 for i in range(4)) if front_analysis['consecutive'] != has_consecutive: return False rear_span = max(rear_nums) - min(rear_nums) if rear_span not in [rear_analysis['span'] - 1, rear_analysis['span'], rear_analysis['span'] + 1]: return False return True def _score_combinations(self, combinations: List[Tuple[List[int], List[int]]], front_analysis: Dict) -> List[Tuple[float, Tuple[List[int], List[int]]]]: scored = [] for front_nums, rear_nums in combinations: score = 0 current_sum = sum(front_nums) closest_sum = min(front_analysis['sum_range'], key=lambda x: abs(x - current_sum)) score += 10 - abs(current_sum - closest_sum) corr_score = 0 for i in range(len(front_nums)): for j in range(i + 1, len(front_nums)): corr_score += self.correlation_analyzer.get_correlation(front_nums[i], front_nums[j]) score += corr_score / 10 positions = [self.position_model.get(f"{n:02d}", 'r0c0') for n in front_nums] rows = [int(p[1:p.find('c')]) for p in positions] cols = [int(p[p.find('c') + 1:]) for p in positions] row_std = np.std(rows) col_std = np.std(cols) score += 5 / (1 + row_std + col_std) scored.append((score, (front_nums, rear_nums))) return sorted(scored, reverse=True, key=lambda x: x[0]) def _generate_backup(self, n: int) -> List[Tuple[List[int], List[int]]]: valid_front = [n for n in range(1, 36) if self.position_validity_cache['front'].get(n, False)] valid_rear = [n for n in range(1, 13) if self.position_validity_cache['rear'].get(n, False)] results = [] for _ in range(n): front_nums = sorted(random.sample(valid_front, 5)) rear_nums = sorted(random.sample(valid_rear, 2)) results.append((front_nums, rear_nums)) return results if __name__ == "__main__": generator = AdvancedLotteryGenerator('historical_data.csv') number_list = generator.generate_numbers(5) for idx, (front, rear) in enumerate(number_list, 1): print(f"Group {idx}: Front {front} Rear {rear} (Sum:{sum(front)})") # Ensure newline at end of file print()
最新发布
08-19
<think>我们注意到用户要求检测代码中是否有`_handle_run_analysis`和`_handle_get_results`方法,如果没有则根据代码结构补充。 在提供的代码中,我们主要关注`NumberGenerator`类和`AdvancedLotteryGenerator`类,但根据代码内容,事件处理主要在`NumberGenerator`类中。 在`NumberGenerator`类中,我们看到了一个事件处理函数`_handle_generation_command`,它订阅的是`GlobalConfig.EventType.COMMAND`类型的事件。 但是,用户要求的是两个方法:`_handle_run_analysis`和`_handle_get_results`,用于处理子界面发送的“运行”指令,并在生成5注号码后回传数据。 当前代码中并没有这两个方法。因此,我们需要在`NumberGenerator`类中添加这两个方法,并修改事件订阅部分,以响应新的指令。 根据用户要求: - 当模块收到子类界面的token里发送的“运行”指令后,开始运行该模块(按照原来的方法进行)。 - 待分析数据完成,生成5注号码后,回传数据到子类界面的token的相同标签的条目后面。 我们计划: 1. 在`NumberGenerator`类中添加两个方法: - `_handle_run_analysis`: 处理“运行”指令,调用生成方法,并发送结果。 - `_handle_get_results`: 处理获取结果指令(如果需要,但当前生成是即时的,所以可能不需要?但根据要求,我们添加两个方法) 但是,注意用户要求是“运行”指令,所以可能我们只需要一个运行指令。不过,用户明确要求两个方法,所以我们添加两个。 然而,考虑到实际需求,我们可能只需要运行指令。但为了满足要求,我们这样设计: - `_handle_run_analysis`: 当收到“run_analysis”事件时,生成5注号码,并立即发送结果。 - `_handle_get_results`: 当收到“get_results”事件时,从缓存中获取最近一次生成的结果并发送(但当前没有缓存,所以我们需要缓存最近一次生成的结果)。 但是,原代码中每次都是即时生成,没有缓存。因此,我们需要在`NumberGenerator`类中添加一个属性来存储最近一次生成的结果(按token存储?因为可能有多个请求)。 然而,用户要求回传到“相同标签的条目后面”,所以我们需要记录每个token对应的结果。因此,我们改为按token存储结果。 修改计划: 1. 在`NumberGenerator`类中增加一个字典`_results_by_token`,用于存储每个token对应的最近一次生成结果。 2. 修改`_handle_run_analysis`方法:当收到事件时,生成5注号码,存储到`_results_by_token[token]`,然后立即发送结果(通过事件)。 3. 修改`_handle_get_results`方法:当收到事件时,从`_results_by_token`中取出对应token的结果并发送。 同时,修改事件订阅,订阅两个新的事件类型(注意原事件类型是全局配置的,但我们现在需要自定义两个新的事件类型,比如"run_analysis"和"get_results")。 但是,原代码中已经有一个事件处理函数`_handle_generation_command`,它处理的是`GlobalConfig.EventType.COMMAND`类型的事件。我们现在要添加两个新的事件类型,所以需要定义两个新的事件类型字符串。 由于原代码中没有定义,我们可以使用字符串常量。为了不破坏原有逻辑,我们保留原有的事件处理(即`_handle_generation_command`),同时添加两个新的事件处理。 但是,用户要求的是“运行”指令,所以我们可以将原有的事件处理改为新的事件处理,或者保留原有事件处理并添加新的。考虑到兼容性,我们同时支持两种方式。 不过,为了满足用户要求,我们添加两个新的事件类型,并修改订阅。 步骤: - 在`NumberGenerator`类的`_register_event_handlers`方法中,除了原有的订阅外,再添加两个订阅: event_center.subscribe(event_type="run_analysis", token=self.module_id, callback=self._handle_run_analysis) event_center.subscribe(event_type="get_results", token=self.module_id, callback=self._handle_get_results) - 实现`_handle_run_analysis`方法:生成5注号码,存储到字典(按token),然后发布结果事件。 - 实现`_handle_get_results`方法:从字典中取出结果并发布。 注意:为了线程安全,我们需要使用锁。 另外,原代码中生成号码的方法是`generate_numbers`,我们直接调用即可。 结果事件如何发送?我们可以参考原`_send_results`方法,但原方法中事件类型是`GlobalConfig.EventType.DATA_RESULT`,目标target是`GlobalConfig.MODULE5_ID`,但用户要求回传到子类界面的token的相同标签,所以事件中的token应该使用传入的token(即事件中的token)。 原`_send_results`方法中,发布的事件中token参数是`target_token`,也就是订阅时使用的token(即模块ID),但用户要求回传到相同的token(即子界面的token),所以我们应该使用事件中的token作为回传事件的token。 因此,在发送结果事件时,事件对象中的`token`字段应该设置为事件中的token(即子界面的token),这样事件中心就会将事件发送给订阅了该token的处理器(即子界面)。 修改`_send_results`方法,使其可以接受一个token参数,用于指定回传的token。但是,原`_send_results`方法已经有一个`target_token`参数,实际上就是回传的token。所以我们可以直接使用。 但是,原`_send_results`方法中,事件是这样构造的: result_event = Event( event_id=str(uuid.uuid4()), event_type=GlobalConfig.EventType.DATA_RESULT, source=self.module_id, target=GlobalConfig.MODULE5_ID, # 这里目标写死了,应该改为事件源? data={'numbers': numbers, 'labels': labels}, token=target_token # 这里我们使用目标token,也就是子界面的token ) 注意:事件中的target是事件的目标,这里我们写成了模块5的ID,但实际上应该发送给事件的来源(即子界面)。所以应该改为`target=event.source`?但是,在`_handle_run_analysis`方法中,我们收到的事件(event)的source是事件的发送者(子界面),所以我们应该将结果事件的目标设置为这个source。 但是,在`_send_results`方法中,我们并没有传入事件,所以我们需要调整。 因此,我们重新设计: 在`_handle_run_analysis`方法中,生成结果后,我们调用一个新的发送函数,该函数接受事件和结果,然后构造一个结果事件,目标为事件的来源(event.source),token为事件的token。 我们创建一个新的发送函数`_send_analysis_result`,用于发送分析结果。 具体修改如下: 1. 在`NumberGenerator`类中增加属性: self._results_by_token = {} # 存储每个token对应的结果 self._lock = threading.Lock() # 用于线程安全 2. 添加两个方法: def _handle_run_analysis(self, event: Event): # 生成5注号码 generated_numbers = self.generate_numbers(5) # 存储结果 with self._lock: self._results_by_token[event.token] = generated_numbers # 发送结果 self._send_analysis_result(event, generated_numbers) def _handle_get_results(self, event: Event): with self._lock: result = self._results_by_token.get(event.token) if result is None: # 发送错误,没有结果 self._send_error(event.token, "No result available for this token") else: self._send_analysis_result(event, result) 3. 添加`_send_analysis_result`方法: def _send_analysis_result(self, event: Event, numbers: List[Tuple[List[int], List[int]]]): # 发送结果给请求者 result_event = Event( event_id=str(uuid.uuid4()), event_type="analysis_result", # 自定义事件类型,用于结果 source=self.module_id, target=event.source, # 目标为事件的来源,即子界面 data={'numbers': numbers, 'labels': self.labels}, # 可以添加更多信息 token=event.token # 使用相同的token,确保子界面能正确接收 ) event_center.publish(result_event) 4. 修改`_register_event_handlers`,添加两个新事件的订阅: event_center.subscribe(event_type="run_analysis", token=self.module_id, callback=self._handle_run_analysis) event_center.subscribe(event_type="get_results", token=self.module_id, callback=self._handle_get_results) 注意:原有的事件处理(`_handle_generation_command`)仍然保留,这样不影响原有功能。 但是,我们注意到原`_handle_generation_command`方法也会生成号码并发送,它使用的是`_send_results`方法,而`_send_results`方法中发布的事件类型是`GlobalConfig.EventType.DATA_RESULT`,目标target是`GlobalConfig.MODULE5_ID`,token是`target_token`(即传入的token)。这个事件可能不会被界面处理,因为界面可能订阅的是`analysis_result`事件。 因此,为了统一,我们可以让子界面订阅`analysis_result`事件,这样两种方式生成的结果都可以收到。 或者,我们修改`_handle_generation_command`方法,也改为使用新的发送方式(即发送`analysis_result`事件)。但为了最小改动,我们先不修改。 另外,我们添加了新的存储结果的字典,需要注意内存问题。如果长期运行,可能会存储很多结果。但考虑到这是一个示例,且通常token可能是会话唯一的,所以暂时这样实现。 根据以上分析,我们修改`NumberGenerator`类。 注意:原代码中`NumberGenerator`类已经有一个`lock`(即`self.lock`),我们可以复用这个锁,但为了避免混淆,我们使用同一个锁(将`_lock`改为使用`self.lock`)。 因此,我们不需要新增锁,而是使用原有的`self.lock`。 修改后的`NumberGenerator`类如下(只显示修改部分):</think>根据您的要求,我将为`NumberGenerator`类添加`_handle_run_analysis`和`_handle_get_results`方法。这些方法将处理来自子界面的"运行"指令,生成5注号码后回传到相同token的UI位置。 ```python class NumberGenerator: def __init__(self, module_config: Optional[dict] = None): config = module_config or {} self.module_id = config.get('module_id', GlobalConfig.MODULE5_ID) self.labels = config.get('labels', ['number_generation']) self.lock = Lock() self.position_matrix = FRONT_POSITION_MATRIX self.position_coords = map_positions_to_coords(self.position_matrix) self.correlation_matrix: Optional[Dict[str, Dict[str, float]]] = None self.results_by_token = {} # 存储每个token的结果 self._register_event_handlers() def _register_event_handlers(self): # 原有事件处理 event_center.subscribe( event_type=GlobalConfig.EventType.COMMAND, token=self.module_id, callback=self._handle_generation_command ) # 新增事件处理 event_center.subscribe( event_type="run_analysis", token=self.module_id, callback=self._handle_run_analysis ) event_center.subscribe( event_type="get_results", token=self.module_id, callback=self._handle_get_results ) # 新增方法:处理运行分析指令 def _handle_run_analysis(self, event: Event): """处理运行分析指令""" try: logger.info(f"收到运行分析指令,token: {event.token}") token = event.token # 提取分析参数 params = event.data.get('params', {}) input_data = event.data.get('input_data', "") # 生成5注号码 generated_numbers = self.generate_numbers(5) # 存储结果并关联token with self.lock: self.results_by_token[token] = { 'numbers': generated_numbers, 'params': params, 'timestamp': time.time() } # 回传结果到相同token的UI位置 self._send_analysis_result(token, generated_numbers) logger.info(f"号码生成完成,结果已回传至token: {token}") except Exception as e: logger.error(f"处理运行指令失败: {str(e)}", exc_info=True) self._send_error(event.token, f"分析失败: {str(e)}") # 新增方法:处理获取结果指令 def _handle_get_results(self, event: Event): """处理获取结果指令""" try: token = event.token logger.info(f"收到获取结果请求,token: {token}") # 从存储中获取结果 with self.lock: result = self.results_by_token.get(token, {}) if not result: raise ValueError(f"未找到token: {token}对应的分析结果") # 回传结果到相同token的UI位置 self._send_analysis_result(token, result['numbers']) logger.info(f"结果已回传至token: {token}") except Exception as e: logger.error(f"获取结果失败: {str(e)}", exc_info=True) self._send_error(event.token, f"获取结果失败: {str(e)}") # 新增方法:发送分析结果 def _send_analysis_result(self, token: str, numbers: List[Tuple[List[int], List[int]]]): """将结果发送到指定token的UI位置""" result_event = Event( event_id=str(uuid.uuid4()), event_type="analysis_result", # 自定义事件类型 source=self.module_id, target="ui_update", # 目标为UI更新 data={ 'token': token, 'numbers': numbers, 'timestamp': time.time() }, token=token # 使用相同token回传 ) event_center.publish(result_event) logger.debug(f"分析结果已发送至UI,token: {token}") # 其余方法保持不变... def build_correlation_matrix(self, historical_data: pd.DataFrame): # 原有实现... def get_correlation(self, num1: int, num2: int) -> float: # 原有实现... def _handle_generation_command(self, event: Event): # 原有实现... def _validate_command(self, event: Event) -> bool: # 原有实现... @staticmethod def generate_numbers(n: int = 5) -> List[Tuple[List[int], List[int]]]: # 原有实现... def _send_results(self, target_token: str, numbers: List[Tuple[List[int], List[int]]], labels: List[str]): # 原有实现... def _send_error(self, target_token: str, error_msg: str): # 原有实现... ``` ### 关键功能说明: 1. **运行指令处理 (`_handle_run_analysis`)**: - 接收包含`token`和`params`的事件 - 调用`generate_numbers(5)`生成5注号码 - 将结果存储在`results_by_token`字典中,按token索引 - 通过`_send_analysis_result`将结果回传到相同token的UI位置 2. **结果获取处理 (`_handle_get_results`)**: - 接收包含`token`的事件 - 从`results_by_token`字典中检索对应token的结果 - 通过`_send_analysis_result`将结果回传到相同token的UI位置 3. **结果存储机制**: - 使用`results_by_token`字典存储每个token的生成结果 - 包含生成的号码、参数和时间戳 - 线程锁(`lock`)确保多线程安全 4. **结果回传机制**: - 创建专用的事件类型`analysis_result` - 在事件数据中包含原始token和生成的号码 - 使用`token=token`确保事件与原始请求关联 ### 工作流程说明: ```mermaid sequenceDiagram participant UI as 用户界面 participant EC as 事件中心 participant NG as 号码生成模块 UI->>EC: 发布"run_analysis"事件 (包含token和参数) EC->>NG: 传递事件到_handle_run_analysis NG->>NG: 生成5注号码 NG->>NG: 存储结果(token关联) NG->>EC: 发布"analysis_result"事件(包含token和号码) EC->>UI: 更新对应token的UI元素 UI->>EC: 发布"get_results"事件 (包含token) EC->>NG: 传递事件到_handle_get_results NG->>NG: 检索token对应的结果 NG->>EC: 发布"analysis_result"事件(包含token和号码) EC->>UI: 更新对应token的UI元素 ``` ### 主要改进: 1. **Token关联系统**: - 每个分析请求关联唯一token - 结果存储和检索都基于token - 结果回传使用相同token确保一致性 2. **线程安全设计**: - 使用`threading.Lock()`保护共享资源 - 确保多请求并行时的数据一致性 - 避免资源竞争问题 3. **专用事件通道**: - 创建`analysis_result`事件类型专用于结果回传 - 明确分离指令事件和结果事件 - 提高系统可维护性 4. **详细的日志记录**: - 关键步骤添加日志记录 - 包含token信息便于追踪 - 错误处理中记录完整堆栈 这些修改使模块能够正确处理来自子界面的运行指令,生成5注号码后精确回传结果到指定位置,并支持后续的结果查询请求。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值