2024年1月

本实验主要使用STM32CubeMX软件配置STM32F407开发板实现RTC周期唤醒、闹钟A/B事件功能,周期唤醒中输出RTC时间,闹钟A/B事件发生时利用串口输出闹钟A/B事件发生提示

1、准备材料

开发板(
正点原子stm32f407探索者开发板V2.4

ST-LINK/V2驱动
STM32CubeMX软件(
Version 6.10.0

keil µVision5 IDE(
MDK-Arm

CH340G Windows系统驱动程序(
CH341SER.EXE

XCOM V2.6串口助手

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板实现RTC周期唤醒、闹钟A/B事件功能,具体为在周期唤醒时利用串口输出当前RTC记录时间,当闹钟A/B事件发生时利用串口输出闹钟A/B事件发生提示

3、实验流程

3.0、前提知识

RTC的时钟可以由外部低速时钟LSE、外部高速时钟HSE经过2-31分频和内部RC振荡LSI三种时钟来源提供,但是一般我们都选择使用32.768kHz的LSE作为RTC的时钟源,
因为32.768kHz的时钟频率可以经过128次分频,然后再经过256次分频得到一个较为精确的1Hz信号
,此信号1s脉动一次,可以方便的用于更新日历,如下图所示
(注释1)

另外
RTC还有两个可编程的闹钟A/B
,如果设置了闹钟A/B的时间,则闹钟A/B设定时间会和当前日历时间对比,如果时间相等,会产生ALRA/BF事件

周期唤醒可以使用RTC内部一个16位唤醒自动重载寄存器来实现,周期唤醒的时钟信号可以来自于更新日历的1Hz(ck_spre)信号,也可以使用RTC时钟的2/4/8/16分频后的时钟,设置该自动重载寄存器的值,
根据时钟频率向上计数,当计数溢出时发生周期唤醒事件

闹钟A/B,周期唤醒产生的 ALRAF、 ALRBF和WUTF事件均可以输出到复用引脚RTC_AF1(PC13)

STM32F407的RTC还有20个32位的备份寄存器
,其名字从RTC_BKP_DR0到RTC_BKP_DR19,定义在stm32f4xx_hal_rtc_ex.h文件中,RTC和备份寄存器均由单片机的备用电源VBAT提供,主电源VDD/VDDA断开不影响备份寄存器内容存储及RTC的正常运行

3.1、CubeMX相关配置

请先阅读“
STM32CubeMX教程1 工程建立
”实验3.4.1小节配置RCC和SYS

3.1.1 、时钟树配置

本文实验中
RTC时钟信号源选择为外部32.768kHz的低速时钟LSE
,与之前使用的STM32F407G-DISC1开发板在RCC及Clock Configuration页面中对LSE的设置不同,首先需要在Pinout & Configuration页面左边System Core/RCC中将原来Disable状态的Low Speed Clock(LSE)选择为Crystal/Ceramic Resonator,表示外部低速时钟LSE由32.768kHz的晶振提供,如下图所示

然后还是在这个页面,在Timers/RTC中单击Activate Clock Source,激活时钟源之后才可以对Clock Configuration页面的时钟修改,如下图所示

最后在Clock Configuration页面将输出到RTC时钟的时钟源选择为LSE,此时就已经配置好了RTC的输入时钟为32.768kHz的LSE,如下图所示

3.1.2、外设参数配置

本实验需要需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”

单击Pinout & Configuration页面左边Timers/RTC

在该页面中间RTC Mode and Configuration中单击Activate Calendar激活日历,这里Alarm A、Alarm B和WakeUp均有Disable、Internal Alarm/WakeUp和Routed to AF1三个选项,分别表示不使用、单纯内部使用和输出到复用引脚AF1(PC13),注意由于AF1只有一个所以一旦某一个选择输出到了复用引脚AF1,其他便不可以设置

配置如下图所示

然后对启用的日历、Alarm A、Alarm B和WakeUp参数做不同的配置,这里比较通俗易懂,具体配置请看下图

3.1.3 、外设中断配置

在Pinout & Configuration页面左边System Core/NVIC中勾选闹钟A/B中断及周期唤醒中断,然后选择合适的中断优先级即可,另外串口中断可以不打开,本节实验输出采用阻塞传输数据的方式输出RTC时间

3.2、生成代码

请先阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节配置Project Manager

单击页面右上角GENERATE CODE生成工程

3.2.1、外设初始化函数调用流程

主函数中调用MX_RTC_Init()函数对RTC基本参数及日历时间、日历日期、闹钟A定时时间、闹钟B定时时间和周期唤醒等参数初始化/使能

在初始化RTC的函数HAL_RTC_Init()中调用了HAL_RTC_MspInit()函数完成了对RTC时钟使能,NVIC使能,NVIC优先级设置

如下图所示为上述的函数调用流程

3.2.2、外设中断函数调用流程

在stm32f4xx_it.c文件中新增了周期唤醒中断服务函数RTC_WKUP_IRQHandler()

在该RTC_WKUP_IRQHandler()函数中调用了HAL_RTCEx_WakeUpTimerIRQHandler()函数处理周期回调事件

最终调用了
虚函数HAL_RTCEx_WakeUpTimerEventCallback()
,该函数需要用户重新实现

如下图所示为周期唤醒中断函数调用流程

同时在stm32f4xx_it.c文件中新增了RTC闹钟A/B事件中断服务函数RTC_Alarm_IRQHandler()

在该RTC_Alarm_IRQHandler()函数中调用了HAL_RTC_AlarmIRQHandler()函数处理闹钟A/B事件

最后在该函数中调用了
虚函数HAL_RTC_AlarmAEventCallback()处理闹钟A事件,调用虚函数HAL_RTCEx_AlarmBEventCallback()处理闹钟B事件

如下图所示为RTC闹钟A/B事件中断函数调用流程

3.2.3、添加其他必要代码

重新实现周期唤醒中断回调函数HAL_RTCEx_WakeUpTimerEventCallback()在rtc.c中,具体实现代码如下图所示

源代码如下

/*周期唤醒回调函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
    RTC_TimeTypeDef sTime;
    RTC_DateTypeDef sDate;
    if(HAL_RTC_GetTime(hrtc, &sTime, RTC_FORMAT_BIN) == HAL_OK)
    {
        HAL_RTC_GetDate(hrtc, &sDate,  RTC_FORMAT_BIN);
        char str[22];
        sprintf(str,"RTC Time= %2d:%2d:%2d\r\n",sTime.Hours,sTime.Minutes,sTime.Seconds);
        printf("%s", str);
    }
    HAL_GPIO_TogglePin(RED_LED_GPIO_Port,RED_LED_Pin);
}

重新实现闹钟A/B事件中断回调函数HAL_RTC_AlarmAEventCallback()和HAL_RTCEx_AlarmBEventCallback()在rtc.c中,具体代码如下所示

源代码如下

/*闹钟A事件回调函数*/
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
{
    char infoA[]="Alarm A(xx:xx:15) trigger: \r\n";
    printf("%s", infoA);
    HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port,GREEN_LED_Pin);
}
 
/*闹钟B事件回调函数*/
void HAL_RTCEx_AlarmBEventCallback(RTC_HandleTypeDef *hrtc)
{
    char infoB[]="Alarm B(xx:0:30) trigger: \r\n";
    printf("%s", infoB);
    HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port,GREEN_LED_Pin);
}

此时的代码可以正常运行,但存在一个问题,复位后重新执行RTC初始化函数会对RTC时间强制初始化为0:0:0,日期也会强制初始化,而我们想要设定的是当我们需要其初始化时就初始化,当一次初始化完毕之后,我不希望每次单片机复位时重新初始化

因此我们可以通过上述介绍的备份寄存器实现此功能,我们在RTC通用初始化结束之后,RTC日期和时间初始化之前处,添加判断RTC备份寄存器是否已被写入1来决定是否需要初始化时间和日期,如果已被写入1,则表示之前已完成日期和时间初始化,不需要再次重新初始化,因此启动周期唤醒后直接退出函数,如下图代码所示

源代码如下

//读取备份寄存R0
uint32_t iniRTC=HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);	
//非零
if((iniRTC & 0x01))  
{
    //使能周期唤醒
    if(HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 0, RTC_WAKEUPCLOCK_CK_SPRE_16BITS) != HAL_OK)
        Error_Handler();
 
    //提前退出函数,不初始化时间和日期
    return;  
}

何时改变/写入RTC备份寄存器中的值呢?

这里笔者使用按键来控制,当按下WK_UP按键时,就翻转备份寄存器RTC_BKP_DR0中存储的值,也就是说按下一次WK_UP按键,备份寄存器RTC_BKP_DR0中的值会在0/1之间改变,如下图所示为主循环中的按键扫描程序

源代码如下

uint32_t iniRTC = HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR0);
iniRTC = !iniRTC;
 
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == 1)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == 1)
    {
        HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR0, iniRTC);
        printf("Write RTC_BKP_DR0 %d\r\n", iniRTC);
        while(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin));
    }
}

4、常用函数

/*RTC周期回调中断服务函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
 
/*RTC闹钟A中断服务函数*/
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
 
/*RTC闹钟B中断服务函数*/
void HAL_RTCEx_AlarmBEventCallback(RTC_HandleTypeDef *hrtc)
 
/*查询RTC时间*/
HAL_StatusTypeDef HAL_RTC_GetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format)
 
/*查询RTC日期*/
HAL_StatusTypeDef HAL_RTC_GetDate(RTC_HandleTypeDef *hrtc, RTC_DateTypeDef *sDate, uint32_t Format)
 
/*读RTC备份寄存器的值*/
uint32_t HAL_RTCEx_BKUPRead(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister)
 
/*写RTC备份寄存器的值*/
void HAL_RTCEx_BKUPWrite(RTC_HandleTypeDef *hrtc, uint32_t BackupRegister, uint32_t Data)

5、烧录验证

5.1、具体步骤

“RCC中启用LSE -> RTC中激活时钟源 -> Clock Configuration配置RTC时钟来源为LSE -> RTC中激活日历 -> 选择闹钟A/B、唤醒模式 -> 配置RTC、闹钟A/B和周期唤醒参数 -> NVIC中启动RTC闹钟A/B、周期唤醒中断 -> rtc.c中重新实现周期唤醒回调函数HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) -> 重新实现闹钟A事件回调函数HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc) -> 重新实现闹钟B事件回调函数HAL_RTCEx_AlarmBEventCallback(RTC_HandleTypeDef *hrtc) -> 在三个回调函数中利用串口编程实现输出信息提示”

5.2、实现现象

烧录程序,通过串口助手观察串口输出信息,每隔1秒,串口助手收到开发板传来的RTC时间信息,并且红色LED每一秒状态翻转一次,当时间到达0:0:15时,闹钟A触发,此时绿色LED灯状态翻转被点亮,随着时间继续流逝,当时间到达0:0:30时,闹钟B触发,此时此时绿色LED灯状态翻转被熄灭,此后每分钟的第15秒闹钟A会触发一次,每小时的0分30秒闹钟B会触发一次

按下WK_UP按键可以翻转备份寄存器RTC_BKP_DR0内存储的值,当备份寄存器RTC_BKP_DR0的值为1时,复位之后RTC的时间不会重置为0;

而当备份寄存器RTC_BKP_DR0的值为0时,复位之后RTC的时间会被重新初始化为0:0:0,串口输出信息如下图所示

细心的小伙伴可能发现0:0:15时刻的闹钟A没有响应,这是因为备份寄存器RTC_BKP_DR0的值为1时,我们在MX_RTC_Init初始化函数中初始化完毕RTC之后直接启动了周期唤醒然后整个函数就退出了,并没有对RTC的闹钟A/B进行初始化,如果你想兼顾两者功能,也可以编写程序不直接退出,而是绕过RTC时间和日期赋初值的代码,然后执行RTC的闹钟A/B的初始化

6、注释详解

注释1
:图片来源STM32F4xx中文参考手册 RM0090

参考资料

STM32Cube高效开发教程(基础篇)

更多内容请浏览
OSnotes的CSDN博客

本文分享自华为云社区《
不得不知的十个常见PY编码习惯
》,作者:码乐。

简介

语言在发展和变化,编码习惯也在发生改变。这里简单聊聊 17个python中常见的编码习惯或者风格。

1,可变数据结构: 注意在函数变量不要使用它

def foo(x=[]):
x.append(
1)
print(x)
>>>foo()
[
1]>>>foo()
[
1,1]>>>foo()

[
1,1,1]

def foo(p
=None):if p isNone:
p
=[]
p.append(
1)
print(p)

foo()
[
1]
foo([
2,3,4])
[
2,3,4,1]

1.1模块的循环导入

我们定义一个老王模块,再定义一个小李模块,相互导入时,将报错

# laowang.py
import xiaoli
count
=4def main():
wilma.pr(
'Hello')if __name__ == '__main__':
main()

# xiaoli.py
import laowang
def pr(str):
print(str
*fired.count)if __name__== '__main__':
pr(
"Ok")

导入问题,如果在使用其他语言 比如 shell 脚本引用 python代码时,需要将python项目加入 linux环境变量。 <=python2.7

export PYTON_PATH=$PYTHON_PATH:/xxx/xxx/XXXProject

或者编辑 python虚拟环境,添加
setup.py
, 然后执行 python install -e .

1.2 基础规则

该次执行不缓存sys.stdout,直接输出控制台

@参数 -u unbuffered

执行时带参数m,以Script方式执行py模块

@参数 -m 

跳过py模块第一行,允许使用非unix形式

@参数 -x

实体具体选项(set implementation-specific option)

@参数 -X

程序读取文件内容并执行

file 

程序从stdin 读取

@参数 -

python3 --help # 显示所有参数

dd

2, 内存管理

python 内存回收 基于 引用计数 和 分级回收。

2.1 小的整数 和短小字符,python将缓存以便重复使用

并且is关键字,用于判断两个引用所指对象是否相同

    >>> a = 'gd'
>>> b = 'gd'
>>> a isb
True

== 只判断值是否相同

    a2 =[]
b2
=[]>>> a2 isb2
False
>>> b2 ==a2
True

2.2 对象引用时python基本构成方式

赋值的本质 a=1,实际上时修改globals()字典的值,局部变量值的修改locals()的访问和修改

>>>a1
very good man
>>> globals()['a1'] = 'bad good mm' >>> globals()

2.3 引用对象reference count

sys.getrefcount() 查看对象的引用计数,sys.getrefcount()在查看某个引用时将创建一个临时引用,所以引用计数将多1

    >>> c=[1,2,3]

getrefcount© #这里c只有一次引用,但是计数时2,因为当前查询有一次临时引用

   2

引用计数的减少和增加

   >>> d=c

getrefcount© # 增加引用d,计数3

     3

del d #删除引用d,c的引用计数又重新为2

2.4 垃圾回收机制 garbage collection

垃圾回收时py 独占进行的,大大降低py效率,特定条件下自动启动垃圾回收。

手工回收 gc.collect()

py 运行时分配对象obj allocation 和取消分配对象 deallocation次数被记录,高于垃圾回收阈值,启动垃圾回收

gc.get_threshold()查看该阈值,gc.set)threshold()重新设置该阈值

(700, 10, 10) 700表示启动垃圾回收阈值,10,10表示分别回收的阈值

2.4.1 垃圾回收分代策略 generation,基本假设如下

存活时间越久,越不可能在后面程序中变为垃圾。 这样所有对象分为0,1,2三代,所有新建对象都是0代。某一对象经历垃圾回收,仍然存活,那么它就被归入下一代对象。

垃圾回收时,一定扫描0代对象,如果0代经过一定次数垃圾回收,下一次对0代和1代扫描清理,

当1代也经历一次次数垃圾回收,这启动0,1,2所有对象的扫描

以上gc.get_threshold()返回(700,10,10)两个表示每10次0代垃圾回收,将配置1次1代垃圾回收,每10次1代垃圾回收,才有1次2代垃圾回收。

2.4.2 两个对象相互引用,

相互引用构成引用环 reference cycle,该引用环将给上一节2.4.1的垃圾回收带来困难,

引用环可能构成一些无法使用但引用计数不为0的对象 为了回收该引用环对象,

py复制了每个对象的引用计数为 gc_ref 遍历所有对象,将每个对象引用的对象相应的gc_ref减1,遍历结束后,

gc_ref不为0的对象和这些对象的引用对象,以及继续更新下游引用对象,被爆了,其他对象被回收。

参考 内存管理,函数默认参数,动态类型

2.4.3 迭代

可迭代对象 iter

迭代器 iter + next

生成器 特殊迭代器 yield

def yd():
a
= 100 yieldayield a*8 yield 8000

2.4.4 抽象方法*(函数)

对象方法(self.对象方法),类方法(@classmethod),静态方法(@staticmethod)

抽象方法 需要子类实现的方法 用@abc.abstractmethod以及 metaclass = abc.ABCMeta
使得任何继承自父类的子类必须覆盖实现抽象方法,否则抛出异常

3,其他概念

闭包 closure

闭包指的是 难以读取其他函数内部遍历的函数

实现: 定义在函数内的内部函数可以读取外层函数变量,从而实现闭包

4,惯例

1,判定dict的key是否存在,使用key in dict而不用 has_key

2,not的位置,使用key not in dict 而不用 no key in dict

3, 使用 dict.get(key[,default])如果key存在,返回,否则返回default

4, 数组字典初始化

 dic ={}for k,v indata:
group
=dic.setdefault(key,[]) #如果存在,返回dic[key],不存在把dic[key]设为defalut并返回
group.append(v)
fromcollections import defaultdic
dic
=defalutdic(list)for (k,v) indata:
dic[key].append(v) #所有key都有一个默认值

迭代一个数组,使用for i,e in enumerate(array) 而不是 for i in range(len(array))

  enumerate 还有第二个参数

5,py3元组unpack

    first, second,*rest, last = range(10)0    1    2~8        9

6, 函数参数传入

def foo(x,y):
print(x,y)
adict
= {'x':1, 'y':2}

foo(**adict) #字典key作为参数名传入参数值

  alist=[1,2]
foo(
*alist)

7, 字符串连接

    name = "Wang" "Hone"  # WangHong

8, 解释器中的

上一次接收器的返回值

9,嵌套列表推导式

 [(i,j) for i in range(3) for j in range(i)]

10, print重定向

print >>open('a.txt', 'w+'), 'hello,world'

11,反射

#检查是否某个自定的类

isinstance(obj,
class)

12,Picking是Python数据结构的序列化过程

存储一个对象,稍后再取出读取

如何pickle 已存在的对象类型到文件

    json = {'name':'jack', 'age':100}
json_file
= open('json pkl', 'rb')
pickle.dump(json.json_file)
json_file.dump(json,json_file)
json_file.close()

取出

data=pickle.load(json_file)
print(data)
json_file.close()

pickler内建类型和外部方法

类自定义行为

__getinitargs__(self)  #
__getnewargs__(self)

Slate 记住它曾经是什么,以及什么时候赋值给它 ?

 __slate__

为对象类 提供限制,只能赋予固定的属性名称

13,对象模型Python3和Python2.x之间的主要区别

Python3的string和unicode区别不复存在,因此__unicode__被取消 __bytes__加入进来(与

python2.7的__str__和__unicode__行为类似),用于心的创建字节数组的内建方法

py3默认除法变成了true除法,因此__div__取消

__coerce__被取消,因为与其他魔法方法有功能重复
cmp 取消,与其他魔法方法功能重复
__nonzero__被重命名为

bool

14,断点

6.1, 断点设置后,代码执行到该位置,程序挂起检查程序行为6.2, 异常断点,到达断点后要执行的操作6.3,  断点属性,达到断点时要执行的操作
挂起策略,用于定义在遇到断点时是否必须挂起应用程序
对其他断点的依赖,
何时必须击中断点
6.4消息记录
断点命中消息,命中断点时,控制输出一条日志消息
堆栈跟踪 断点的堆栈跟踪将命中打印到控制台
6.5断点工具
pdb 交互式代码调试,功能包括
设置断点,单步调试,进入函数调试,查看当前代码,查看栈片段,动态改变变量的值
进入pdb交互界面
命令
break或b设置断点continue或c 继续执行
list或l 查看当前代码段
step或s 进入函数
return或r 执行代码直到当前函数返回
exit或q 中止并退出
next或n 执行下一行
pp 打印变量的值

15, 依赖问题

pycurl 需要contos 7安装python3-devel
具体过程pycurl-centos7

16, py编译 发布流程

1,编译生成pyc文件,建议增加-O优选项

python3 -O -m compileall -b .

2, 删除py文件

   find . -name "*.py"|xargs rm -rf

3, 删除__pycache__目录

   find . -name "__pycache__" |xargs rm -rf

4, 打包 tar包

cd ..
tar
-cjvf xxx.1.1.00.tar.bz2 xxx

5, 或git push到仓库

  git push origin master:master

16 常用内置 魔法函数

iter # 在类中实现,可以直接对类进行迭代 类似于如下形式

            obj.next 或  next(obj)

call # 在对象直接 进行调用

            obj()

、slate 为对象类 提供限制,只能赋予固定的属性名称

__qualname__  # 查看类和当前函数名
>>>D.f.__qualname__>>>   D.f#返回类和函数

通过类字典返回函数,不会 返回函数名

    >>> D.__dict__['f']<function D.f at 0x000001F6C6224670

通过类实例的点运算 查看函数 将直接返回函数

>>> d =D()>>>d.f<bound method D.f of <__main__.D object at 0x000001F6C5F42070>>

查看实例的函数对象

>>>d.f.__func__<function D.f at 0x000001F6C6224670>
>>>d.f.__self__<__main__.D object at 0x000001F6C5F42070>

17 内置函数 二进制运算

chr
返回整数 i 的字符串格式,如 chr(
97) 返回字符 'a'是 ord的逆运算
chr(
'8364') (欧元符号)返回 €
ord
返回代表单个Unicode字符的 码点的整数,例如 ord(
'a')返回整数 97ord('') (欧元符号)返回 8364

点击关注,第一时间了解华为云新鲜技术~

决策树分类
算法是一种监督学习算法,它的基本原理是将数据集通过一系列的问题进行拆分,这些问题被视为决策树的叶子节点和内部节点。
决策树的每个分支代表一个可能的决策结果,而每个叶子节点代表一个最终的分类结果。

决策树分类
算法的历史可以追溯到1980年代初,当时研究者开始探索用机器学习来解决分类问题。
在1981年,J.Ross Quinlan开发了
ID3算法
,该算法使用信息增益来选择决策树的最佳划分属性。
后来,在1986年,J.Ross Quinlan提出了
C4.5算法
,该算法引入了剪枝技术,以防止过拟合,该算法还引入了处理连续属性、缺失数据和多值属性等新特性。
在1998年,Jerome Friedman等人提出了
CART算法

Classification and Regression Trees
),该算法采用了二叉树,使得决策树更加简洁和易于解释。

1. 算法概述

决策树
不仅可以用在
分类问题
上,也可以用在
回归问题
上。
关于
决策树

回归问题
上的应用,可以参考:
TODO

回到
决策树分类
算法上来,构建决策树的有
三种
算法:

1.1. ID3

ID3算法
的完整名称是
Iterative Dichotomiser 3
,即迭代二叉树3代。
ID3算法
的核心思想是以
信息增益
来度量属性的选择,选择分裂后信息增益最大的属性进行分裂。

对于任意样本数据
\(x(x_1,x_2,...,x_n)\)
,它的
信息熵
定义为:
\(entropy(x) = -\sum_{i=1}^n p_i\log_2(p_i)\)

基于信息熵,
信息增益
的公式为:
\(IG(T) = entropy(S) - \sum_{value(T)}\frac{|S_x|}{|S|}entropy(S_x)\)
其中:

  • \(S\)
    表示全部样本的集合
  • \(|S|\)
    表示
    \(S\)
    中样本数量
  • \(T\)
    表示样本的某个特征
  • \(value(T)\)
    表示特征
    \(T\)
    所有的取值集合
  • \(S_x\)

    \(S\)
    中特征
    \(T\)
    的值为
    \(x\)
    的样本的集合
  • \(|S_x|\)
    表示
    \(S_x\)
    中样本数量

1.2. C4.5

C4.5算法
是以
ID3算法
为基础的,它改为使用
信息增益率
来作为决策树分裂的依据。
这样,就克服了
ID3算法
中信息增益选择属性时
偏向选择取值多的属性
的不足。

C4.5算法
中引入了一个分裂信息(
split information
)的项来惩罚取值较多的特征:
\(SI(T) = - \sum_{value(T)}\frac{|S_x|}{|S|}\log\frac{|S_x|}{|S|}\)

基于此,
信息增益率
的公式为:
\(gainRatio(T)=\frac{IG(T)}{SI(T)}\)
\(IG(T)\)
就是上一节
ID3算法
中的
信息增益
公式。

1.3. CART

CART算法
全称是
classification and regression tree
(分类与回归树)。
这个算法既可以用来分类,也可以用来回归,在回归问题上的介绍可以参考。

CART算法
是根据基尼系数(Gini)来划分特征的,每次选择基尼系数最小的特征作为最优切分点。
其中基尼系数的计算方法:
\(gini(p) = \sum_{i=1}^n p_i(1-p_i)=1-\sum_{i=1}^n p_i^2\)

2. 创建样本数据


scikit-learn
中的样本生成器
make_classification
来生成分类用的样本数据。

import matplotlib.pyplot as plt
from sklearn.datasets import make_classification

# 分类数据的样本生成器
X, y= make_classification(n_samples=1000, n_classes=4, n_clusters_per_class=1, n_informative=6)
plt.scatter(X[:, 0], X[:, 1], marker="o", c=y, s=25)

plt.show()

image.png
关于
样本生成器
的详细内容,请参考:
TODO

3. 模型训练

首先,分割
训练集

测试集

from sklearn.model_selection import train_test_split

# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

这次按照
8:2的比例
来划分训练集和测试集。

然后用
不同的算法
来训练决策树模型:

from sklearn.tree import DecisionTreeClassifier

reg_names = [
    "ID3算法",
    "C4.5算法",
    "CART算法",
]

# 定义
regs = [
    DecisionTreeClassifier(criterion="entropy"),
    DecisionTreeClassifier(criterion="log_loss"),
    DecisionTreeClassifier(criterion="gini"),
]

# 训练模型
for reg in regs:
    reg.fit(X_train, y_train)

# 在测试集上进行预测
y_preds = []
for reg in regs:
    y_pred = reg.predict(X_test)
    y_preds.append(y_pred)

for i in range(len(y_preds)):
    correct_pred = np.sum(y_preds[i] == y_test)
    print("【{}】 预测正确率:{:.2f}%".format(reg_names[i], correct_pred / len(y_pred) * 100))

# 运行结果
【ID3算法】 预测正确率:71.50%
【C4.5算法】 预测正确率:72.50%
【CART算法】 预测正确率:75.00%

算法的正确率
差别不是特别大。
感兴趣的朋友,可以尝试调整样本生成器部分,生成一些特征较多的数据来看看算法之间的性能差别。

4. 总结

决策树分类
算法广泛应用于图像识别、文本分类、语音识别、信用评分、疾病诊断等众多领域。
例如,在电商平台上,可以通过决策树分类算法对用户的行为数据进行挖掘和分析,实现对用户的精准推荐;
在医疗领域,可以通过对医学数据的分析,辅助医生进行疾病诊断和治疗方案制定。

决策树分类
算法的
优势
有:

  1. 易于理解和解释
    ,直观地展示出分类的过程
  2. 对于数据集可以进行
    并行处理
    ,提高了算法的效率
  3. 对于缺失数据和非数值属性有很好的
    处理能力
  4. 可以处理
    多分类
    问题

决策树分类
算法也存在一些
劣势

  1. 可能存在
    过拟合
    ,需要使用剪枝技术来控制
  2. 可能存在
    偏向性
    ,需要使用加权投票来处理
  3. 对于连续属性和多值属性处理起来
    比较复杂
    ,需要额外的处理方法
  4. 大规模数据集处理起来
    比较耗时
    ,需要优化算法或者使用分布式计算等方法

事故还原

近日,白泽在使用 docker 的时候,开放了防火墙的端口,以 SSH 方式访问远程服务器的 docker 守护进程(无需使用密钥即可建立连接),随后竟遭到了挖矿木马的攻击,好一顿折腾之后,使用 TLS 证书加密通信才解决了问题。

时间线

image-20240106125725464

部分腾讯云短信和云服务器安全主机管理后台的截图(失眠.jpg):

image-20240105231542444

TLS 实践

TLS For Docker:
https://docs.docker.com/engine/security/protect-access/

TLS 的主要作用是提供加密、认证和数据完整性,以确保在网络上进行的通信是安全的。下面的实践在你对照复现的时候,白泽建议你同时打开 docker 的文档,一并参考,注意命令格式。

为了允许机器 A 通过 TLS 访问机器 B 上的 Docker,你需要在机器 B 上进行一系列设置,以确保 Docker 守护程序(Docker Daemon)使用 TLS,并为客户端提供相应的证书。

image-20240106110427612

服务端(docker 守护进程对应服务器)配置

一、在 B 机器上生成 CA(Certificate Authority)证书和密钥

openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
  • 生成 rsa 私钥


    • 私钥生成:rsa
    • 私钥位数:4096
    • 加密私钥:aes256(
      生成过程中会提示输入密码,用于对私钥进行加密,后续使用私钥时也会提示输入这个密码
  • 生成自签名证书(根证书 CA)


    • 格式:x509
    • 签名:使用 sha256 计算 hash 摘要,对摘要使用私钥进行加密,加密的过程就是签名
    • 作用:作为根证书用于对后续 server 和 client 端的证书进行签名

二、生成 Docker 守护程序的未签名证书和密钥

openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=主机名" -sha256 -new -key server-key.pem -out server.csr
  • 创建服务器密钥
  • 生成服务器证书签名请求(CSR)文件:以便后续将其发送给证书颁发机构 (CA) 进行签名,从而得到服务器的证书

三、创建一个扩展属性配置文件(extfile.cnf),用于指定 IP 地址和 DNS 名称

echo subjectAltName = IP:机器B的IP地址,DNS:机器B的DNS名 >> extfile.cnf
echo extendedKeyUsage = serverAuth >> extfile.cnf
  • 创建一个
    extfile.cnf
    配置文件,其中包含主题备用名称(subjectAltName)的信息,包括 DNS 名称和 IP 地址。
  • echo extendedKeyUsage = serverAuth >> extfile.cnf
    :添加一个扩展属性,指定此证书仅用于服务器认证

四、生成 Docker 守护程序的签名证书

openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
    -CAcreateserial -out server-cert.pem -extfile extfile.cnf
  • 使用 CA 的私钥 (
    ca-key.pem
    ) 和 CA 签名证书 (
    ca.pem
    ) 对服务器的 CSR (
    server.csr
    ) 进行签名,生成服务器的证书 (
    server-cert.pem
    )。此命令还使用了
    extfile.cnf
    文件中的配置信息,确保证书包含了正确的主题备用名称和扩展属性。

五、配置 Docker 守护程序

在 Docker 守护程序的启动配置中添加 TLS 相关的参数:

dockerd \
    --tlsverify \
    --tlscacert=ca.pem \
    --tlscert=server-cert.pem \
    --tlskey=server-key.pem \
    -H=0.0.0.0:2376

防火墙配置:
如果机器 B 上有防火墙,确保允许来自机器 A 的流量通过 2376 端口。

项目中采用 wss 来建立的前后端连接, 但是并没有用到认证的证书, 所以自己用 openssl 生成了私钥, 自签名证书来使用:

这里就不再赘述 Wss 连接过程, 直接上手操作:

1. 生成私钥, 证书:

请查看分类 "开发工具" => 使用 openssl 安装和生成证书

2. rust 服务端:

将生成的 server.crt 和 server.key 放到
d:\\User\\Desktop\\wss\\openssl 目录下

依赖:

[dependencies]
rustls = "0.19.0"
tokio-rustls = "0.22.0"
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.20"
futures-util = "0.3"

RUST 代码:

use futures_util::sink::SinkExt;
use futures_util::stream::{Stream, StreamExt};
use rustls::ServerConfig;
use std::fs::File;
use std::io::BufReader;
use std::sync::Arc;
use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::TlsAcceptor;
use tokio_tungstenite::accept_async;
use tokio_tungstenite::tungstenite::protocol::Message;


#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 加载SSL keys
let certs = rustls::internal::pemfile::certs(&mut BufReader::new(File::open(
"d:\\User\\Desktop\\wss\\openssl\\server.crt",
)?))
.unwrap();
let key = rustls::internal::pemfile::pkcs8_private_keys(&mut BufReader::new(File::open(
"d:\\User\\Desktop\\wss\\openssl\\server.key",
)?))
.unwrap()[0]
.clone();


let tls_cfg = {
let mut cfg = ServerConfig::new(rustls::NoClientAuth::new());
cfg.set_single_cert(certs, key).expect("error setting cert");
Arc::new(cfg)
};
let tls_acceptor = TlsAcceptor::from(tls_cfg);


// 设置TCP监听器
let addr = "127.0.0.1:10096";
let listener = TcpListener::bind(&addr).await?;
println!("Listening on: {}", addr);


while let Ok((stream, _)) = listener.accept().await {
let acceptor = tls_acceptor.clone();


tokio::spawn(async move {
if let Ok(tls_stream) = acceptor.accept(stream).await {
let websocket_result = accept_async(tls_stream).await;


match websocket_result {
Ok(mut ws_stream) => {
println!("WebSocket connection established");
while let Some(message_result) = ws_stream.next().await {
match message_result {
Ok(message) => {
if message.is_text() || message.is_binary() {
// 对接收到的消息进行处理
println!("接收到消息: {}", message);
// 回传消息
println!("回传消息: {}", message);
if let Err(_) = ws_stream.send(message).await {
// 处理错误
break;
}
}
}
Err(e) => {
// 处理错误
println!("Error: {}", e);
break;
}
}
}
}
Err(e) => {
println!("Error during the websocket handshake occurred: {}", e);
}
}
}
});
}


Ok(())
}

3. Angular 客户端:

Angular 配置:

将生成的

client.crt 和 client.key 放到和 package.json 同级文件夹下.

package.json

"scripts": {
"start": "ng serve --host 0.0.0.0 --port 443 --ssl true --ssl-cert ./client.crt --ssl-key ./client.key",
}

// 补充

./client.crt

./client.key
是相对路径,表示这些证书文件位于当前工作目录。当前工作目录指的是你在运行
ng serve
命令时所在的目录,也就是命令行提示符下的目录。如果你在项目的根目录下运行这个命令(通常是包含
package.json
文件的目录),
./
就表示这个项目的根目录

angular 代码:

import {webSocket} from "rxjs/webSocket";

ngOnInit() { this.websocketTest(); } websocketTest() { const url: string = 'wss://127.0.0.1:10096'; const myWebSocket = webSocket({ url: url, openObserver: { next: () => { console.info('WebSocket 连接已建立: ' + url); }, error: (err) => { console.error('WebSocket 连接出错: ' + url); console.error(err); }, }, }); const data = JSON.stringify({name: '张三', age: 18, sex: '男'}); myWebSocket.next(data); console.log('客户端发送请求: ', data); myWebSocket.subscribe( next => { console.log('客户端收到响应: ', next); }, error => { console.error('WebSocket 出错: ' + url); console.error(error); } ); }

4. 谷歌浏览器设置

谷歌浏览器图标右击 属性 -> 快捷方式 -> 目标(修改其中字符串, )

原来类似 "C:\Program Files\Google\Chrome\Application\chrome.exe"

修改为 "C:\Program Files\Google\Chrome\Application\chrome.exe" --ignore-certificate-errors

这个命令会忽略证书错误。正常情况下,当你访问一个使用自签名或无效证书的网站时,Chrome 会显示一个警告页面,指示你有潜在的安全风险。然而,使用
--ignore-certificate-errors
参数可以忽略这些证书错误,并允许你继续访问网站,而不会受到浏览器的证书警告页面的干扰。

5. 结果展示:

前端项目使用 npm start 启动:

前端控制台:

后端控制台:

版权声明:本文为博客园博主「书源」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
rust angular 自签名证书 wss - 书源 - 博客园 (cnblogs.com)