首页
📁归档
⏳时光机
🚩友链
📫留言
📧订阅本站
推荐
📕考研课程
🏜️ 免费壁纸
❤ 捐助本站
💰资助名单
🎵音乐实验
Search
1
【NPN/PNP三极管】放大电路饱和失真和截止失真的区别
20,043 阅读
2
论文写作中如何把word里面所有数字和字母替换为新罗马字体
10,606 阅读
3
【高数】形心计算公式讲解大全
9,037 阅读
4
Vivado-FPGA Verilog烧写固化教程
7,869 阅读
5
【概论】一阶矩、二阶矩原点矩,中心矩区别与概念
7,805 阅读
🪶微语&随笔
励志美文
我的随笔
写作办公
📡电子&通信
嵌入式&系统
通信&信息处理
编程&脚本笔记
⌨️IC&系统
FPGA&ASIC
VLSI&IC验证
EDA&虚拟机
💻电子&计算机
IP&SOC设计
机器学习
软硬件算法
登录
/
注册
刘航宇(共311篇)
找到
311
篇与
刘航宇
相关的结果
- 第 3 页
2023-12-11
华为C++算法-识别有效的IP地址和掩码并进行分类统计
问题 注意: 输入描述: 输出描述: 示例 需要注意的细节 思路 具体实现 代码 问题 请解析IP地址和对应的掩码,进行分类识别。要求按照A/B/C/D/E类地址归类,不合法的地址和掩码单独归类。 所有的IP地址划分为 A,B,C,D,E五类 A类地址从1.0.0.0到126.255.255.255; B类地址从128.0.0.0到191.255.255.255; C类地址从192.0.0.0到223.255.255.255; D类地址从224.0.0.0到239.255.255.255; E类地址从240.0.0.0到255.255.255.255 私网IP范围是: 从10.0.0.0到10.255.255.255 从172.16.0.0到172.31.255.255 从192.168.0.0到192.168.255.255 子网掩码为二进制下前面是连续的1,然后全是0。(例如:255.255.255.32就是一个非法的掩码) (注意二进制下全是1或者全是0均为非法子网掩码) 注意: 类似于【0...】和【127...】的IP地址不属于上述输入的任意一类,也不属于不合法ip地址,计数时请忽略 私有IP地址和A,B,C,D,E类地址是不冲突的 输入描述: 多行字符串。每行一个IP地址和掩码,用~隔开。 输出描述: 统计A、B、C、D、E、错误IP地址或错误掩码、私有IP的个数,之间以空格隔开。 示例 输入: 10.70.44.68~255.254.255.0 1.0.0.1~255.0.0.0 192.168.0.2~255.255.255.0 19..0.~255.255.255.0 输出: 1 0 1 0 0 2 1 说明: 10.70.44.68~255.254.255.0的子网掩码非法,19..0.~255.255.255.0的IP地址非法,所以错误IP地址或错误掩码的计数为2; 1.0.0.1~255.0.0.0是无误的A类地址; 192.168.0.2~255.255.255.0是无误的C类地址且是私有IP; 所以最终的结果为1 0 1 0 0 2 1 示例2 输入: 0.201.56.50~255.255.111.255 127.201.56.50~255.255.111.255 输出: 0 0 0 0 0 0 0 说明: 类似于【0...】和【127...】的IP地址不属于上述输入的任意一类,也不属于不合法ip地址,计数时请忽略 需要注意的细节 类似于【0...】和【127...】的IP地址不属于上述输入的任意一类,也不属于不合法ip地址,计数时可以忽略 私有IP地址和A,B,C,D,E类地址是不冲突的,也就是说需要同时+1 如果子网掩码是非法的,则不再需要查看IP地址 全零【0.0.0.0】或者全一【255.255.255.255】的子网掩码也是非法的 思路 按行读取输入,根据字符‘~’ 将IP地址与子网掩码分开 查看子网掩码是否合法。 合法,则继续检查IP地址 非法,则相应统计项+1,继续下一行的读入 查看IP地址是否合法 合法,查看IP地址属于哪一类,是否是私有ip地址;相应统计项+1 非法,相应统计项+1 具体实现 判断IP地址是否合法,如果满足下列条件之一即为非法地址 数字段数不为4 存在空段,即【192..1.0】这种 某个段的数字大于255 判断子网掩码是否合法,如果满足下列条件之一即为非法掩码 不是一个合格的IP地址 在二进制下,不满足前面连续是1,然后全是0 在二进制下,全为0或全为1 如何判断一个掩码地址是不是满足前面连续是1,然后全是0? 将掩码地址转换为32位无符号整型,假设这个数为b。如果此时b为0,则为非法掩码 将b按位取反后+1。如果此时b为1,则b原来是二进制全1,非法掩码 如果b和b-1做按位与运算后为0,则说明是合法掩码,否则为非法掩码 代码 注意getline函数可以指定分割字符串的字符 // 引入输入输出流、字符串、字符串流和向量等头文件 #include<iostream> #include<string> #include<sstream> #include<vector> // 使用标准命名空间 using namespace std; // 定义一个函数,判断一个字符串是否是合法的IP地址 bool judge_ip(string ip){ // 定义一个整数变量,记录IP地址的段数 int j = 0; // 定义一个字符串流对象,用于分割IP地址 istringstream iss(ip); // 定义一个字符串变量,用于存储IP地址的每一段 string seg; // 使用循环,以'.'为分隔符,获取IP地址的每一段 while(getline(iss,seg,'.')) // 如果段数加一大于4,或者该段为空,或者该段的数值大于255,说明不是合法的IP地址,返回false if(++j > 4 || seg.empty() || stoi(seg) > 255) return false; // 如果循环结束后,段数等于4,说明是合法的IP地址,返回true return j == 4; } // 定义一个函数,判断一个字符串是否是私有的IP地址 bool is_private(string ip){ // 定义一个字符串流对象,用于分割IP地址 istringstream iss(ip); // 定义一个字符串变量,用于存储IP地址的每一段 string seg; // 定义一个整数向量,用于存储IP地址的每一段的数值 vector<int> v; // 使用循环,以'.'为分隔符,获取IP地址的每一段,并将其转换为整数,存入向量中 while(getline(iss,seg,'.')) v.push_back(stoi(seg)); // 如果IP地址的第一段等于10,说明是私有的IP地址,返回true if(v[0] == 10) return true; // 如果IP地址的第一段等于172,并且第二段在16到31之间,说明是私有的IP地址,返回true if(v[0] == 172 && (v[1] >= 16 && v[1] <= 31)) return true; // 如果IP地址的第一段等于192,并且第二段等于168,说明是私有的IP地址,返回true if(v[0] == 192 && v[1] == 168) return true; // 如果以上条件都不满足,说明不是私有的IP地址,返回false return false; } // 定义一个函数,判断一个字符串是否是合法的子网掩码 bool is_mask(string ip){ // 定义一个字符串流对象,用于分割IP地址 istringstream iss(ip); // 定义一个字符串变量,用于存储IP地址的每一段 string seg; // 定义一个无符号整数变量,用于存储IP地址的二进制表示 unsigned b = 0; // 使用循环,以'.'为分隔符,获取IP地址的每一段,并将其转换为整数,左移8位后与b进行按位或运算,得到IP地址的二进制表示 while(getline(iss,seg,'.')) b = (b << 8) + stoi(seg); // 如果b等于0,说明不是合法的子网掩码,返回false if(!b) return false; // 将b按位取反后加一,得到b的补码 b = ~b + 1; // 如果b等于1,说明不是合法的子网掩码,返回false if(b == 1) return false; // 如果b与b减一进行按位与运算,结果等于0,说明b只有一个1,说明是合法的子网掩码,返回true if((b & (b-1)) == 0) return true; // 如果以上条件都不满足,说明不是合法的子网掩码,返回false return false; } // 定义主函数 int main(){ // 定义一个字符串变量,用于存储输入的IP地址和子网掩码 string input; // 定义七个整数变量,用于统计A、B、C、D、E类地址、错误地址和私有地址的个数 int a = 0,b = 0,c = 0,d = 0,e = 0,err = 0,p = 0; // 使用循环,读取输入的IP地址和子网掩码,直到输入结束 while(cin >> input){ // 定义一个字符串流对象,用于分割IP地址和子网掩码 istringstream is(input); // 定义一个字符串变量,用于存储IP地址或子网掩码 string add; // 定义一个字符串向量,用于存储IP地址和子网掩码 vector<string> v; // 使用循环,以'~'为分隔符,获取IP地址和子网掩码,并存入向量中 while(getline(is,add,'~')) v.push_back(add); // 如果IP地址或子网掩码不合法,错误地址的个数加一 if(!judge_ip(v[1]) || !is_mask(v[1])) err++; else{ // 如果IP地址不合法,错误地址的个数加一 if(!judge_ip(v[0])) err++; else{ // 获取IP地址的第一段的数值 int first = stoi(v[0].substr(0,v[0].find_first_of('.'))); // 如果IP地址是私有的,私有地址的个数加一 if(is_private(v[0])) p++; // 根据IP地址的第一段的数值,判断IP地址的类别,并相应的类别的个数加一 if(first > 0 && first <127) a++; else if(first > 127 && first <192) b++; else if(first > 191 && first <224) c++; else if(first > 223 && first <240) d++; else if(first > 239 && first <256) e++; } } } // 输出A、B、C、D、E类地址、错误地址和私有地址的个数 cout << a << " " << b << " " << c << " " << d << " " << e << " " << err << " " << p << endl; // 返回0,表示程序正常结束 return 0; }
嵌入式&系统
编程&脚本笔记
软硬件算法
# 嵌入式
# 笔试面试
# C/C++
刘航宇
3年前
0
584
1
2023-12-11
大疆题解:跨时钟域脉冲信号处理—脉冲同步器(快到慢)
问题描述 题解1.1 电路波形图 1.2 代码 注意 问题描述 sig_a 是 clka(300M)时钟域的一个单时钟脉冲信号(高电平持续一个时钟clka周期),请设计脉冲同步电路,将sig_a信号同步到时钟域 clkb(100M)中,产生sig_b单时钟脉冲信号(高电平持续一个时钟clkb周期)输出。请用 Verilog 代码描述。 clka时钟域脉冲之间的间隔很大,无需考虑脉冲间隔太小的问题。 电路的接口如下图所示: 图片 题解 1.1 电路波形图 图片 如上图所述,aclk快时钟域发送的信号signal_a,慢时钟域的时钟bclk根本就采集不到,此时不能使用打两拍的方式,要想办法转换思路, 如果能够让同步于快时钟域aclk下的脉冲信号signal_a变长到可以让慢时钟域bclk检测到,那么这个问题就可以完美解决了。 所以先将快时钟域clka下的脉冲信号signal_a,在快时钟域clka的作用下,变为沿信号,产生一个名为adata的中间变量来作为脉冲信号signal_a的沿信号。如上图所示,每当快时钟域aclk检测到signal_a脉冲信号为高时,让adata信号取反,使得signal_a的第一个脉冲变为adata信号的上升沿,signal_a的第二个脉冲变为adata信号的下降沿,后面如果Signal_a信号还有脉冲依然是变为adata信号的上升沿和下降沿。 巧妙的利用将“脉冲信号”转化为“沿信号”的思想就可以使慢时钟域的时钟bclk检测到同步于快时钟域aclk且将脉冲信号signal_a转化为沿信号adata, 相当于是把同步于快时钟域aclk的脉冲信号signal_a进行了展宽处理,这样我们就把快时钟域aclk的脉冲信号signal_a通过adata信号“沿”的形式在慢时钟域bclk中得到了保留。 接着,我们再对adata信号做打两拍的处理就可以将adata信号同步到慢时钟域clkb中了。bdata0信号是adata信号在慢时钟域bclk下打的第一拍,bdata1信号是adata信号在慢速时钟域bclk下打第二拍,bdata1就是同步于慢速时钟域bclk的稳定信号。 最后,采用 边沿检测 的方法,将变为bdata1信号的“沿”再转化为脉冲信号,这里我们使用的方法是采用异或门。需要注意的是不能直接使用bdata0和bdata1来产生沿标志信号,因为bdata0信号的不稳定性可能会导致产生的沿信号也不稳定,所以需要将bdata1信号再打一拍,产生signal_b信号。 1.2 代码 //快时钟数据同步 module pulse_detect( input clka , input clkb , input rst_n , input sig_a , output sig_b ); wire sig_a; reg adata; reg bdata0; reg bdata1; reg bdata2; always @(posedge clka or negedge rst_n) begin if(~rst_n) begin adata <= 1'd0; end else begin adata <= adata ^ sig_a; end end always @(posedge clkb or negedge rst_n) begin if(~rst_n) begin bdata0 <= 1'd0; bdata1 <= 1'd0; bdata2 <= 1'd0; end else begin bdata0 <= adata; bdata1 <= bdata0; bdata2 <= bdata1; end end assign sig_b = bdata1 ^ bdata2; endmodule注意 signal_a是两个脉冲,但是使用“脉冲同步”同步到bclk时钟域确只有一个脉冲了,在使用“脉冲同步”时应注意这一点。所以,脉冲同步一般适用于单比特信号从快时钟域传递慢时钟域的场景。
FPGA&ASIC
# ASIC/FPGA
# Verilog
刘航宇
3年前
0
1,169
4
2023-11-20
超声模块HC_SR04基本原理与FPGA、STM32应用
HC-SR04硬件概述 接口定义: 模式选择: 测量操作: FPGA实现超声测距模块代码 STM32(Cubemax)实现超声波测距CubeMX配置STM32 编写代码 HC-SR04 硬件概述 HC-SR04超声波距离传感器的核心是两个超声波传感器。一个用作发射器,将电信号转换为40 KHz超声波脉冲。接收器监听发射的脉冲。如果接收到它们,它将产生一个输出脉冲,其宽度可用于确定脉冲传播的距离。就是如此简单! 该传感器体积小,易于在任何机器人项目中使用,并提供2厘米至600厘米(约1英寸至13英尺)之间出色的非接触范围检测,精度为3mm。 图片 接口定义: 图片 模式选择: 图片 测量操作: 一:GPIO模式 图片 外部MCU给模块Trig脚一个大于10uS的高电平脉冲;模块会给出一个与距离等比的高电平脉冲信号,可根据脉宽时间“T” 算出: 距离=T*C/2 (C为声速) 声速温度公式:c=(331.45+0.61t/℃)m•s-1 (其中330.45是在0℃) 0℃声速: 330.45M/S 20℃声速: 342.62M/S 40℃声速: 354.85M/S0℃-40℃声速误差7%左右。实际应用,如果需要精确距离值,必需要考虑温度影响,做温度补偿。 二:UART模式 UART 模式波特率设置: 9600 N 1 图片 连接串口。外部MCU或PC发命令0XA0,模块完成测距后发3个返回距离 数据,BYTE_H,BYTE_M与BYTE_L。 距离计算方式如下(单位mm): 距离=((BYTE_H<<16)+(BYTE_M<<8)+ BYTE_L)/1000 三:IIC模式 IIC地址: 0X57 IIC传输格式: 写数据: 图片 读数据: 图片 命令格式: 图片 向模块写入0X01,模块开始测距;等待200mS(模块最大测距时间) 以上。直接读出3个距离数据。BYTE_H,BYTE_M与BYTE_L。 距离计算方式如下(单位mm): 距离=((BYTE_H<<16)+(BYTE_M<<8)+ BYTE_L)/1000 FPGA实现超声测距 本次测距教程一律按基本原理实现,至于UART、ICC测距原理可以网上查询 FPGA 产生周期性的 TRIG 脉冲信号,使得超声波模块周期性发出测距脉冲,当这些脉冲发出后遇到障碍物返回,超声波模块将返回的脉冲处理整形后返回给 FPGA,即 ECHO 信号。我们通过对 ECHO 信号的高脉冲保持时间就可以推算出超声波脉冲和障碍物之间的距离。 本实例的功能如图三所示,FPGA 产生 10us 脉冲 TRIG 给超声波测距模块,然后以 10us 为单位计算超声波测距模块返回的回响信号 ECHO 的高电平保持时间。ECHO 的高电平保持时间通过一定的换算后可以得到障碍物和超声波测距模块之间的距离(由距离公式计算&进制换算模块实现),我们将最终获得的以 mm 为单位的距离信息显示在 4 位数码管上。 图片 模块代码 1、vlg_en模块 /* * @Author: Hangyu Liu * @Date: 2023-11-20 15:24:01 * @Email: hyliu@ee.ac.cn * @Descripttion: 板子时钟转化1us * @Last Modified time: 2023-11-20 15:24:01 */ //1us/50ns=20 module vlg_1us#(parameter P_CLK_PERIORD = 50) //i_clk的时钟周期50ns,20MHZ ( input i_clk, input i_rst_n, output reg o_clk //时钟周期1us ); parameter NUM_DIV = 20;// (1MHZ = 1us,20MHZ/20 = 1MHZ) reg [3:0] cnt; always @(posedge i_clk or negedge i_rst_n) begin if(!i_rst_n) begin cnt <= 4'd0; o_clk <= 1'b0; end else if(cnt == NUM_DIV/2 - 1) begin cnt <= 4'b0; o_clk <= ~o_clk; end else cnt <= cnt + 1'b1; end endmodule2、vlg_trig模块 /* * @Author: Hangyu Liu * @Date: 2023-11-20 16:50:44 * @Email: hyliu@ee.ac.cn * @Descripttion: 产生10us的触发超声信号 * @Last Modified time: 2023-11-20 16:50:44 */ module vlg_trig ( input i_rst_n, input clk_1us, //1us output reg o_trig ); reg[17:0] r_tricnt; //200ms的周期计数 1us一个单位 always @(posedge clk_1us or negedge i_rst_n)begin if(!i_rst_n) r_tricnt <= 18'd0; else if((r_tricnt == 18'd199999)) r_tricnt <= 18'd0; else r_tricnt <= r_tricnt + 1'b1; end //产生保持10us的高脉冲o_tring信号 always @(posedge clk_1us or negedge i_rst_n) begin if(!i_rst_n) o_trig<=1'b0; else if((r_tricnt > 18'd0) && (r_tricnt <= 18'd10)) o_trig <= 1'b1; //不从0开始0~9,防止出现不到10us的波干扰 else o_trig <= 1'b0; end endmodule3、vlg_echo模块 module vlg_echo ( input i_clk, //1us input i_rst_n, input i_clk_1us, input i_echo, output reg[15:0] o_t_us ); reg[1:0] r_echo; wire pos_echo,neg_echo; reg r_cnt_en; reg[15:0] r_echo_cnt; //对i_echo信号同步处理,获取边沿检测信号,产生计数使能信号r_cnt_en always @(posedge i_clk or negedge i_rst_n) begin if(!i_rst_n) r_echo <= 2'd0; else r_echo <= {r_echo[0],i_echo}; //设置两个寄存器进行打拍寄存 end assign pos_echo = r_echo[0] & ~r_echo[1]; //现状态是1上状态是0,就是上升沿 assign neg_echo = ~r_echo[0] & r_echo[1]; always @(posedge i_clk or negedge i_rst_n) begin if(!i_rst_n) r_cnt_en <= 1'b0; else if(pos_echo) r_cnt_en <= 1'b1; else if(neg_echo) r_cnt_en <= 1'b0; else r_cnt_en <= r_cnt_en; end //对i_echo信号的高脉冲计时,以us为单位 always @(posedge i_clk_1us or negedge i_rst_n) begin if(!i_rst_n) r_echo_cnt <= 1'b0; else if(r_cnt_en) r_echo_cnt <= r_echo_cnt + 1'b1; else r_echo_cnt <= 1'b0; end //在下降沿对计数最大值进行保存 always @(negedge i_clk or negedge i_rst_n) begin if(!i_rst_n) o_t_us <= 16'd0; else if(neg_echo) o_t_us <= r_echo_cnt; else o_t_us <= o_t_us; end endmodule4、顶层模块例化 /* * @Author: Hangyu Liu * @Date: 2023-11-23 17:16:40 * @Email: hyliu@ee.ac.cn * @Descripttion:HR04驱动模块 * @Last Modified time: 2023-11-23 17:16:40 */ module vlg_design ( input i_clk, //200MHZ input i_rst_n, input i_echo, //这是超声模块给的输入 output o_trig, output wire[15:0] w_t_us ); wire clk_20MHZ; clk_div_20MHZ UU( .i_clk(i_clk), .i_rst_n(i_rst_n), .clk_div(clk_20MHZ) ); localparam P_CLK_PERIORD = 50; wire clk_1us; //使能时钟产生模块 vlg_1us #( .P_CLK_PERIORD(P_CLK_PERIORD) //i_clk的时钟周期50ns,20MHZ )U1( .i_clk(clk_20MHZ), .i_rst_n(i_rst_n), .o_clk(clk_1us) ); //产生超声波测距模块的触发信号o_trig vlg_trig U2( .i_rst_n(i_rst_n), .clk_1us(clk_1us), .o_trig(o_trig) ); //超声波测距模块的回响信号i_echo的高电平时间采集 vlg_echo U3( .i_clk(clk_20MHZ), .i_rst_n(i_rst_n), .i_clk_1us(clk_1us), .i_echo(i_echo), .o_t_us(w_t_us) ); endmoduleSTM32(Cubemax)实现超声波测距 CubeMX配置STM32 1 时钟配置 这里我用的是STM32F103C8T6的核心板,时钟配置如下图,我用了8MHz的HSE,HCLK调到了最大值72MHz 图片 2 设置输入捕获的定时器 设置定时器TIM2每1us向上计数一次,通道4为上升沿捕获并连接到超声波模块的ECHO引脚,记得开启定时器中断(涉及到捕获中断+定时器溢出中断)。 图片 3 触发引脚 PB10接到了HC-SR04的TIRG触发引脚,默认输出低电平 图片 4 串口配置 还要开启一个串口,以便通过串口查看测距结果 图片 编写代码 hc-sr04.h #ifndef HCSR04_H_ #define HCSR04_H_ #include "main.h" #include "delay.h" typedef struct { uint8_t edge_state; uint16_t tim_overflow_counter; uint32_t prescaler; uint32_t period; uint32_t t1; // 上升沿时间 uint32_t t2; // 下降沿时间 uint32_t high_level_us; // 高电平持续时间 float distance; TIM_TypeDef* instance; uint32_t ic_tim_ch; HAL_TIM_ActiveChannel active_channel; }Hcsr04InfoTypeDef; extern Hcsr04InfoTypeDef Hcsr04Info; /** * @description: 超声波模块的输入捕获定时器通道初始化 * @param {TIM_HandleTypeDef} *htim * @param {uint32_t} Channel * @return {*} */ void Hcsr04Init(TIM_HandleTypeDef *htim, uint32_t Channel); /** * @description: HC-SR04触发 * @param {*} * @return {*} */ void Hcsr04Start(); /** * @description: 定时器计数溢出中断处理函数 * @param {*} main.c中重定义void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim) * @return {*} */ void Hcsr04TimOverflowIsr(TIM_HandleTypeDef *htim); /** * @description: 输入捕获计算高电平时间->距离 * @param {*} main.c中重定义void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) * @return {*} */ void Hcsr04TimIcIsr(TIM_HandleTypeDef* htim); /** * @description: 读取距离 * @param {*} * @return {*} */ float Hcsr04Read(); #endif /* HCSR04_H_ */ hc-sr04.c #include "hc-sr04.h" Hcsr04InfoTypeDef Hcsr04Info; /** * @description: 超声波模块的输入捕获定时器通道初始化 * @param {TIM_HandleTypeDef} *htim * @param {uint32_t} Channel * @return {*} */ void Hcsr04Init(TIM_HandleTypeDef *htim, uint32_t Channel) { /*--------[ Configure The HCSR04 IC Timer Channel ] */ // MX_TIM2_Init(); // cubemx中配置 Hcsr04Info.prescaler = htim->Init.Prescaler; // 72-1 Hcsr04Info.period = htim->Init.Period; // 65535 Hcsr04Info.instance = htim->Instance; // TIM2 Hcsr04Info.ic_tim_ch = Channel; if(Hcsr04Info.ic_tim_ch == TIM_CHANNEL_1) { Hcsr04Info.active_channel = HAL_TIM_ACTIVE_CHANNEL_1; // TIM_CHANNEL_4 } else if(Hcsr04Info.ic_tim_ch == TIM_CHANNEL_2) { Hcsr04Info.active_channel = HAL_TIM_ACTIVE_CHANNEL_2; // TIM_CHANNEL_4 } else if(Hcsr04Info.ic_tim_ch == TIM_CHANNEL_3) { Hcsr04Info.active_channel = HAL_TIM_ACTIVE_CHANNEL_3; // TIM_CHANNEL_4 } else if(Hcsr04Info.ic_tim_ch == TIM_CHANNEL_4) { Hcsr04Info.active_channel = HAL_TIM_ACTIVE_CHANNEL_4; // TIM_CHANNEL_4 } else if(Hcsr04Info.ic_tim_ch == TIM_CHANNEL_4) { Hcsr04Info.active_channel = HAL_TIM_ACTIVE_CHANNEL_4; // TIM_CHANNEL_4 } /*--------[ Start The ICU Channel ]-------*/ HAL_TIM_Base_Start_IT(htim); HAL_TIM_IC_Start_IT(htim, Channel); } /** * @description: HC-SR04触发 * @param {*} * @return {*} */ void Hcsr04Start() { HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); DelayUs(10); // 10us以上 HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); } /** * @description: 定时器计数溢出中断处理函数 * @param {*} main.c中重定义void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim) * @return {*} */ void Hcsr04TimOverflowIsr(TIM_HandleTypeDef *htim) { if(htim->Instance == Hcsr04Info.instance) // TIM2 { Hcsr04Info.tim_overflow_counter++; } } /** * @description: 输入捕获计算高电平时间->距离 * @param {*} main.c中重定义void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) * @return {*} */ void Hcsr04TimIcIsr(TIM_HandleTypeDef* htim) { if((htim->Instance == Hcsr04Info.instance) && (htim->Channel == Hcsr04Info.active_channel)) { if(Hcsr04Info.edge_state == 0) // 捕获上升沿 { // 得到上升沿开始时间T1,并更改输入捕获为下降沿 Hcsr04Info.t1 = HAL_TIM_ReadCapturedValue(htim, Hcsr04Info.ic_tim_ch); __HAL_TIM_SET_CAPTUREPOLARITY(htim, Hcsr04Info.ic_tim_ch, TIM_INPUTCHANNELPOLARITY_FALLING); Hcsr04Info.tim_overflow_counter = 0; // 定时器溢出计数器清零 Hcsr04Info.edge_state = 1; // 上升沿、下降沿捕获标志位 } else if(Hcsr04Info.edge_state == 1) // 捕获下降沿 { // 捕获下降沿时间T2,并计算高电平时间 Hcsr04Info.t2 = HAL_TIM_ReadCapturedValue(htim, Hcsr04Info.ic_tim_ch); Hcsr04Info.t2 += Hcsr04Info.tim_overflow_counter * Hcsr04Info.period; // 需要考虑定时器溢出中断 Hcsr04Info.high_level_us = Hcsr04Info.t2 - Hcsr04Info.t1; // 高电平持续时间 = 下降沿时间点 - 上升沿时间点 // 计算距离 Hcsr04Info.distance = (Hcsr04Info.high_level_us / 1000000.0) * 340.0 / 2.0 * 100.0; // 重新开启上升沿捕获 Hcsr04Info.edge_state = 0; // 一次采集完毕,清零 __HAL_TIM_SET_CAPTUREPOLARITY(htim, Hcsr04Info.ic_tim_ch, TIM_INPUTCHANNELPOLARITY_RISING); } } } /** * @description: 读取距离 * @param {*} * @return {*} */ float Hcsr04Read() { // 测距结果限幅 if(Hcsr04Info.distance >= 450) { Hcsr04Info.distance = 450; } return Hcsr04Info.distance; } main.c 1、引用对应的头文件 /* USER CODE BEGIN Includes */ #include "hc-sr04.h" #include "printf.h" /* USER CODE END Includes */2、200ms测距一次 /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_TIM2_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ DelayInit(72); Hcsr04Init(&htim2, TIM_CHANNEL_4); // 超声波模块初始化 Hcsr04Start(); // 开启超声波模块测距 printf("hc-sr04 start!\r\n"); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { // 打印测距结果 printf("distance:%.1f cm\r\n", Hcsr04Read()); Hcsr04Start(); DelayMs(200); // 测距周期200ms /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } 3、重定义定时器的中断服务函数 /* USER CODE BEGIN 4 */ /** * @description: 定时器输出捕获中断 * @param {TIM_HandleTypeDef} *htim * @return {*} */ void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { Hcsr04TimIcIsr(htim); } /** * @description: 定时器溢出中断 * @param {*} * @return {*} */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim) { Hcsr04TimOverflowIsr(htim); } /* USER CODE END 4 */ 4、串口打印结果 图片
嵌入式&系统
FPGA&ASIC
# ASIC/FPGA
# 嵌入式
刘航宇
3年前
0
1,408
2
嵌入式/SOC开发利器-ZYNQ简介与入门
ZYNQ是什么? ZYNQ为什么厉害 ZYNQ的结构 开发工具 ZYNQ开发流程 ZYNQ是什么? 这是一款由Xilinx公司开发的集成了ARM处理器和FPGA可编程逻辑的片上系统(SoC)芯片。ZYNQ7000有多个型号,根据处理器核心数和FPGA系列的不同,可以应用于多种领域,如图像处理,通信,嵌入式系统等。 ZYNQ中国人读法 “zingke”、“任克”,“Soc”,英文全称叫 System on one Chip ,也就是片上系统的意思。没有微机基础的同学可能不明白什么叫Soc,但是你可以细细琢磨一下,我们的手机和台式电脑的不同,你就可以理解Soc的内含了。 传统计算机是将CPU,内存,GPU,南北桥焊接在印刷电路板上,各个组件之间是分立的。但是Soc则将CPU和各种外设集中到一块芯片上,集合成一个系统,因此像手机这种使用了Soc芯片的这种微机可以做的很轻薄,我们可以说,Soc是未来微机发展的一个趋势,我之前遇见过的像什么全志的A33就是典型的Soc。 ZYNQ为什么厉害 就在于它是一块可编程的Soc。其内部往往有处理器硬核和一些定制外设,并且外设当中有一个很厉害的玩意:PL,即可编程逻辑模块,也就是我们一般意义上的FPGA,所以简单理解ZYNQ就是“ 单片机 + FPGA “,它既可以执行代码程序,也可以实现FPGA。因此我们设计ZYNQ就是在做Soc设计。 ZYNQ的结构 我们先来开一下简化版的模型 图片 上面的模型细致低展开后就是下图的样子: 图片 图是 ZYNQ 7000的结构图,大体分为PS(Processing System)和 PL(Programmable Logic)两部分,其中的PS部分主要是由双核APU和外围的一些外设组成,说实话很像单片机的结构,而外围的PL则类似FPGA,并且两者通过AXI接口进行互联以实现功能. 重点介绍一下APU,应用处理单元:Application Processing Unit,位于PS(processing system)中,包括一个单核或者双核的cortex-A9处理器,处理器连接一个512KB的共享L2cache,每个处理器都有一个32KB的高速L1 cache,A9支持虚拟内存和32bit arm 指令。APU中的A9处理器由可配置的MP组成,MP包含SCU(snoop control unit:监控控制单元)单元,这个单元主要负责获取两个处理器的L1 cache和ACP(accelerator coherency port:加速器相关接口) PL的一致性。应用单元还有一个低延迟的片上memory,与L2 cache并行的,ACP(加速器接口)是PL与APU通信接口,该接口是PL作为主机的AXI协议的接口,最多支持64bit位宽,PL通过ACP接口访问L2 cache 和片上memory,同时保持和L1 cache的内存一致性。L2 cache 可以访问 DDR 控制,这个ddr 控制器是专用的,大大降低内存读写的延迟APU 还包括一个32bit的看门狗,一个64bit的全局定时器,APU 架构图如下所示: 图片 开发工具 在Vivado 19.2之前,我们开发Zynq需要三样必须的软件: Vivado SDK PetaLinux 其中Vivado用来开发硬件平台,SDK开发软件,PetaLinux则制作配套的Linux系统。可能有些人还有用到HLS ,即VIvado HLS 或者Vitis HLS;其中Vivado HLS 2020.1将是Vivado HLS的最后一个版本,取而代之的是VitisHLS。 到了Vivado 19.2之后,事情发生了变化。为了方便大家理解,我愿意称之这些软件成了为Vitis 家族的各个部分,原来的SDK被Vitis IDE取代,Vivado导出的 .hdf 文件被 .xsa文件代替,用来给vitis平台使用。因此我们需要的开发Zynq 最基本的软件变成了 Vivado Vitis IDE PetaLinux 各软件发挥的作用和之前的差不多,不过除了上面提到的四款软件外,Vitis家族还有 Vitis AI 等组件,他们共同组成了所谓的“Vitis™ Unified Software Platform ”,从发展趋势来看,这些开发软件应该会逐步的统一,入门的同学也不会再一头雾水地纠结 Vitis 和 Vivado 的区别和联系了。 ZYNQ开发流程 ZYNQ类似于一个 单片机 + FPGA的结构,其实我觉得如果大家接触过一些 Soc就会更好地理解ZYNQ的作用,就例如全志A33这块Soc,它是一块ASIC,不可以通过编程来对芯片的硬件进行重设计的。 图片 我们可以看到,灰色部分的外设都是固定的,像什么摄像头接口,什么视频接口都是设计好的,定制化的好处就使得总体比较高效,制造成本也低;但是如果我要运用到其它场景下,比如说我需要多个摄像头,那这块芯片就不再适合了(硬件控制的上限就是前后两颗摄像头) 而ZYNQ的意义相当于只给你定制的蓝色部分,也就是处理器内核,灰色的部分都可以通过FPGA实现,这让电子工程师们可以快速开发出各种各样有针对性的Soc;当然了,看过我第一篇博客的同学都知道,其实固定的硬核不止只有处理器内核,其实还有串口和内存控制器之类的外设,这其实是追寻一种固定和变化之间的平衡。 咱们把话说回ZYNQ的开发上来。 图片 ZYNQ的开发流程分为硬件和软件两部分,在SDK之前的属于硬件开发,也就是我们常说的PL部分的开发,而SDK后就属于软件部分的开发了,类似单片机,属于PS部分。当然现在最新的Vitis IDE已经取代了SDK,所以后半部分一般在SDK中进行。 PL部分的开发包括对 嵌入式最小系统的构建,以及FPGA外设的设计两个方面。我觉得要转变的一个思维是,我们现在不是在开发一个什么SDRAM控制器,什么IIC协议控制器,我们在开发的是一个小型的微机系统!因此嵌入式最小系统的设计是我们的核心。 首先,在IP INTEGRATOR中我们要创建BLOCK DESIGN。 图片 IP是用来进行 Embedded System Design ,也就是咱们常说的嵌入式系统设计。也就是咱们上面说的嵌入式最小系统的设计。 图片 大家可以看到,一个最小的系统其实不需要PL参与的,PL可以作为PS的一个外设使用,或者是自己做自己的事情,仅仅作为一个PL工作。既然是外设,当然是可用可不用的,毕竟咱们有好多的外设可以在Block Design 中直接配置使用,即下图绿色部分。 图片 配置好嵌入式系统后,咱们根据需要进行PL部分的设计。这里涉及一个问题,那就是PS和PL之间的数据传输方式有哪些: 中断 IO方式:MIO EMIO GPIO BRAM或FIFO或EMIF AXI DMA:PS通过AXI-lite向AXI DMA发送指令,AXI DMA通过HP通路和DDR交换数据,PL通过AXI-S读写DMA的数据。 等等。。。 可以看出,其实两个部分的交互方式还是很多的,以后咱们遇到一个说一个。 在Vivado端完成对嵌入式系统的设计后,我们就要进入Vitis IDE 端进行软件的开发。 图片 Vitis IDE简单来说流程一般是:新建一个工程,选择Platform ,也就是我们之前在Vivado中生成的 XSA文件,然后添加文件,进行开发。我相信使用过Keil 5的同学们应该心中对文件目录结构应该更胸有成竹,Src文件夹中存放的是源文件。 代码编写完之后是编译,编译完就是下载了。不过这里要注意以下,如果我们使用了PL的资源,那么在下载软件编译生成的 elf 文件之前,需要先下载硬件设计过程中生成的 bitstream 文件,对 PL 部分进行配置。 最后就是验证工作了,上述的流程是普通的ZYNQ开发流程;玩的花一点的同学可能是直接上Linux操作系统,这部分等后面我接触到了再说吧! 其实我觉得ZYNQ入门简单,精通的话需要大量的知识储备,但也不是不可能,开发ZYNQ相比于做单片机开发肯定路子会更广一些,向上可以做IC设计,向下嵌入式、单片机什么的工作也能胜任。
FPGA&ASIC
IP&SOC设计
# ASIC/FPGA
# 嵌入式
# SOC设计
刘航宇
3年前
0
4,631
5
2023-11-01
机器学习/深度学习-10个激活函数详解
图片 综述 1. Sigmoid 激活函数 2. Tanh / 双曲正切激活函数 3. ReLU 激活函数 4. Leaky ReLU 5. ELU 6. PReLU(Parametric ReLU) 7. Softmax 8. Swish 9. Maxout 10. Softplus 综述 激活函数(Activation Function)是一种添加到人工神经网络中的函数,旨在帮助网络学习数据中的复杂模式。类似于人类大脑中基于神经元的模型,激活函数最终决定了要发射给下一个神经元的内容。 在人工神经网络中,一个节点的激活函数定义了该节点在给定的输入或输入集合下的输出。标准的计算机芯片电路可以看作是根据输入得到开(1)或关(0)输出的数字电路激活函数。因此,激活函数是确定神经网络输出的数学方程式,本文概述了深度学习中常见的十种激活函数及其优缺点。 首先我们来了解一下人工神经元的工作原理,大致如下: 图片 上述过程的数学可视化过程如下图所示: 图片 1. Sigmoid 激活函数 lofj8ajx.png图片 Sigmoid 函数的图像看起来像一个 S 形曲线。 函数表达式如下: $$\[\mathrm{f(z)=1/(1+e^{\wedge}-z)}\]$$ 在什么情况下适合使用 Sigmoid 激活函数呢? Sigmoid 函数的输出范围是 0 到 1。由于输出值限定在 0 到 1,因此它对每个神经元的输出进行了归一化; 用于将预测概率作为输出的模型。由于概率的取值范围是 0 到 1,因此 Sigmoid 函数非常合适; 梯度平滑,避免「跳跃」的输出值; 函数是可微的。这意味着可以找到任意两个点的 sigmoid 曲线的斜率; 明确的预测,即非常接近 1 或 0。 Sigmoid 激活函数有哪些缺点? 倾向于梯度消失; 函数输出不是以 0 为中心的,这会降低权重更新的效率; Sigmoid 函数执行指数运算,计算机运行得较慢。 2. Tanh / 双曲正切激活函数 图片 tanh 激活函数的图像也是 S 形,表达式如下: $$f(x)=tanh(x)=\frac{2}{1+e^{-2x}}-1$$ tanh 是一个双曲正切函数。tanh 函数和 sigmoid 函数的曲线相对相似。但是它比 sigmoid 函数更有一些优势。 图片 首先,当输入较大或较小时,输出几乎是平滑的并且梯度较小,这不利于权重更新。二者的区别在于输出间隔,tanh 的输出间隔为 1,并且整个函数以 0 为中心,比 sigmoid 函数更好; 在 tanh 图中,负输入将被强映射为负,而零输入被映射为接近零。 注意:在一般的二元分类问题中,tanh 函数用于隐藏层,而 sigmoid 函数用于输出层,但这并不是固定的,需要根据特定问题进行调整。 3. ReLU 激活函数 图片 ReLU 激活函数图像如上图所示,函数表达式如下: 图片 ReLU 函数是深度学习中较为流行的一种激活函数,相比于 sigmoid 函数和 tanh 函数,它具有如下优点: 当输入为正时,不存在梯度饱和问题。 计算速度快得多。ReLU 函数中只存在线性关系,因此它的计算速度比 sigmoid 和 tanh 更快 当然,它也有缺点: Dead ReLU 问题。当输入为负时,ReLU 完全失效,在正向传播过程中,这不是问题。有些区域很敏感,有些则不敏感。但是在反向传播过程中,如果输入负数,则梯度将完全为零,sigmoid 函数和 tanh 函数也具有相同的问题; 我们发现 ReLU 函数的输出为 0 或正数,这意味着 ReLU 函数不是以 0 为中心的函数。 4. Leaky ReLU 它是一种专门设计用于解决 Dead ReLU 问题的激活函数: 图片 ReLU vs Leaky ReLU 为什么 Leaky ReLU 比 ReLU 更好? $$f(y_i)=\begin{cases}y_i,&\text{if }y_i>0\\a_iy_i,&\text{if }y_i\leq0\end{cases}.$$ 1、Leaky ReLU 通过把 x 的非常小的线性分量给予负输入(0.01x)来调整负值的零梯度(zero gradients)问题; 2、leak 有助于扩大 ReLU 函数的范围,通常 a 的值为 0.01 左右; 3、Leaky ReLU 的函数范围是(负无穷到正无穷)。 注意:从理论上讲,Leaky ReLU 具有 ReLU 的所有优点,而且 Dead ReLU 不会有任何问题,但在实际操作中,尚未完全证明 Leaky ReLU 总是比 ReLU 更好。 5. ELU 图片 ELU vs Leaky ReLU vs ReLU ELU 的提出也解决了 ReLU 的问题。与 ReLU 相比,ELU 有负值,这会使激活的平均值接近零。均值激活接近于零可以使学习更快,因为它们使梯度更接近自然梯度。 图片 显然,ELU 具有 ReLU 的所有优点,并且: 没有 Dead ReLU 问题,输出的平均值接近 0,以 0 为中心; ELU 通过减少偏置偏移的影响,使正常梯度更接近于单位自然梯度,从而使均值向零加速学习; ELU 在较小的输入下会饱和至负值,从而减少前向传播的变异和信息。 一个小问题是它的计算强度更高。与 Leaky ReLU 类似,尽管理论上比 ReLU 要好,但目前在实践中没有充分的证据表明 ELU 总是比 ReLU 好。 6. PReLU(Parametric ReLU) 图片 PReLU 也是 ReLU 的改进版本: $$f(y_i)=\begin{cases}y_i,&\text{if }y_i>0\\a_iy_i,&\text{if }y_i\leq0\end{cases}.$$ 看一下 PReLU 的公式:参数α通常为 0 到 1 之间的数字,并且通常相对较小。 如果 a_i= 0,则 f 变为 ReLU 如果 a_i> 0,则 f 变为 leaky ReLU 如果 a_i 是可学习的参数,则 f 变为 PReLU PReLU 的优点如下: 在负值域,PReLU 的斜率较小,这也可以避免 Dead ReLU 问题。 与 ELU 相比,PReLU 在负值域是线性运算。尽管斜率很小,但不会趋于 0。 7. Softmax 图片 Softmax 是用于多类分类问题的激活函数,在多类分类问题中,超过两个类标签则需要类成员关系。对于长度为 K 的任意实向量,Softmax 可以将其压缩为长度为 K,值在(0,1)范围内,并且向量中元素的总和为 1 的实向量。 图片 Softmax 与正常的 max 函数不同:max 函数仅输出最大值,但 Softmax 确保较小的值具有较小的概率,并且不会直接丢弃。我们可以认为它是 argmax 函数的概率版本或「soft」版本。 Softmax 函数的分母结合了原始输出值的所有因子,这意味着 Softmax 函数获得的各种概率彼此相关。 Softmax 激活函数的主要缺点是: 在零点不可微; 负输入的梯度为零,这意味着对于该区域的激活,权重不会在反向传播期间更新,因此会产生永不激活的死亡神经元。 8. Swish 图片 函数表达式:y = x * sigmoid (x) Swish 的设计受到了 LSTM 和高速网络中 gating 的 sigmoid 函数使用的启发。我们使用相同的 gating 值来简化 gating 机制,这称为 self-gating。 self-gating 的优点在于它只需要简单的标量输入,而普通的 gating 则需要多个标量输入。这使得诸如 Swish 之类的 self-gated 激活函数能够轻松替换以单个标量为输入的激活函数(例如 ReLU),而无需更改隐藏容量或参数数量。 Swish 激活函数的主要优点如下: 「无界性」有助于防止慢速训练期间,梯度逐渐接近 0 并导致饱和;(同时,有界性也是有优势的,因为有界激活函数可以具有很强的正则化,并且较大的负输入问题也能解决); 导数恒 > 0; 平滑度在优化和泛化中起了重要作用。 9. Maxout 图片 在 Maxout 层,激活函数是输入的最大值,因此只有 2 个 maxout 节点的多层感知机就可以拟合任意的凸函数。 单个 Maxout 节点可以解释为对一个实值函数进行分段线性近似 (PWL) ,其中函数图上任意两点之间的线段位于图(凸函数)的上方。 $ReLU=\max\bigl(0,x\bigr),\mathrm{abs}\bigl(x\bigr)=\max\bigl(x,-x\bigr)$ Maxout 也可以对 d 维向量(V)实现: 图片 假设两个凸函数 h_1(x) 和 h_2(x),由两个 Maxout 节点近似化,函数 g(x) 是连续的 PWL 函数。 $$g\bigl(x\bigr)=h_1\bigl(x\bigr)-h_2\bigl(x\bigr)$$ 因此,由两个 Maxout 节点组成的 Maxout 层可以很好地近似任何连续函数。 图片 10. Softplus 图片 Softplus 函数:f(x)= ln(1 + exp x) Softplus 的导数为 f ′(x)=exp(x) / ( 1+exp x ) = 1/ (1 +exp(−x )) ,也称为 logistic / sigmoid 函数。 Softplus 函数类似于 ReLU 函数,但是相对较平滑,像 ReLU 一样是单侧抑制。它的接受范围很广:(0, + inf)。
机器学习
# 机器学习
# 软件算法
# DL/ML
刘航宇
3年前
0
682
0
Python-BP神经网络实现单特征多分类
不同于图像处理,神经网络在处理其他不少领域,常常需要单特征分类,本例实现同半径的圆进行多分类(3分类),特征即为圆的半径。 输入层12节点,一个6节点的隐藏层,输出层3个节点。 1.目标 2.开发环境 3.准备数据 4.处理数据 5.构建BP神经网络 总结 1.目标 通过BP算法实现对不同半径的圆的分类。 2.开发环境 Python3.10;jupyter notebook 3.准备数据 目的: 生成3类圆在第一象限内的坐标(圆心都是原点) 第1类:半径范围为1~10,分类标识为‘0’ 第2类:半径范围为10~20,分类标识为‘1’ 第3类:半径范围为20~30,分类标识为‘2’ 代码如下:data_generate.py import numpy as np import math import random import csv # 只生成第一象限内的坐标即可。每个圆生成12个坐标(x,y),相当于12个特征维度 def generate_circle(lower, upper): # 圆在第一象限内的坐标 data_ur = np.zeros(shape=(12, 2)) # 在上下限范围内,随机产生一个值作为半径 radius = random.randint(int(lower), int(upper)) # 在0~90度内,每隔7.5度取一次坐标,正好取12次 angles = np.arange(0, 0.5 * np.pi, 1 / 24 * np.pi) for i in range(12): temp_ur = np.zeros(2) x = round(radius * math.cos(angles[i]), 2) y = round(radius * math.sin(angles[i]), 2) temp_ur[0] = x temp_ur[1] = y data_ur[i] = temp_ur return data_ur, label # 将坐标保存到CSV文件中 def save2csv(data, batch, label): out = open("D:\\circles.csv", 'a', newline='') csv_write = csv.writer(out, dialect='excel') length = int(data.size / 2) for i in range(length): string = str(data[i][0]) + ',' + str(data[i][1]) + ',' + str(batch) + ',' + str(label) temp = string.split(',') csv_write.writerow(temp) out.close() if __name__ == "__main__": ''' 生成3类圆,标签(label)分别为:0、1、2 第1类圆的半径下限为1,上限为10 第2类圆的半径下限为10,上限为20 第3类圆的半径下限为20,上限为30 圆心都为原点 ''' lower = [1, 10, 20] # 半径随机值的下限 upper = [10, 20, 30] # 半径随机值的上限 label = ['0', '1', '2'] # 种类的标签 for i in range(len(label)): # 每类数据生成50组 for j in range(50): data, label = generate_circle(lower[i], upper[i]) batch = 50 * i + j + 1 # 数据的批次,用来区分每个坐标是属于哪个圆的 save2csv(data, batch, label[i])共3类圆,每类生成50个圆,每个圆 有12个坐标,因此在输出文件D:\circles.csv中总共有3×50×12=1800行数据: 图片 通过生成的坐标绘制散点图如下: 图片 图中蓝色的点是label为0的圆,绿色的点是label为1的圆,红色的点是label为2的圆。 4.处理数据 目标: 根据第3步获得的坐标,计算每个圆的半径(勾股定理)作为神经网络的输入。 代码如下:data_process.py import csv import math def process(file_name): # 要读取的CSV文件 csv_file = csv.reader(open(file_name, encoding='utf-8')) # 要生成的CSV文件 out_file = open("D:\\circles_data.csv", 'a', newline='') csv_write = csv.writer(out_file, dialect='excel') # 将csv_file每一行的圆坐标取出,如果是同一批次的(同一个圆),则写入到out_file的一行中 rows = [row for row in csv_file] current_batch = 'unknown' current_label = 'unknown' data_list = [] for r in rows: # 将无关字符都替换为空格 temp_string = str(r).replace('[', '').replace(']', '').replace('\'', '') # 将字符串以逗号分隔 item = str(temp_string).split(',') # 分别取出x轴坐标、y轴坐标、批次、标签 x = float(item[0]) y = float(item[1]) batch = item[2] label = item[3] # 如果是同一批次(同一个圆),则都放入data_list中 if current_batch == batch: # 根据勾股定理计算半径 distance = math.sqrt(pow(x, 2) + pow(y, 2)) data_list.append(distance) # 如果不是同一批次(同一个圆),则在末尾加上标签后,作为一行写入输出文件 else: if len(data_list) != 0: # 这个地方需注意一下,最后的标签用3列来表示,而不是一列 if label.strip() == '0': data_list.append(1) data_list.append(0) data_list.append(0) elif label.strip() == '1': data_list.append(0) data_list.append(1) data_list.append(0) else: data_list.append(0) data_list.append(0) data_list.append(1) result_string = str(data_list).replace('[', '').replace(']', '').replace('\'', '').strip() csv_write.writerow(result_string.split(',')) # 清空data_list,继续写入下一个批次 data_list.clear() distance = math.sqrt(pow(x, 2) + pow(y, 2)) data_list.append(distance) current_batch = batch current_label = label # 确保最后一个批次的数据能写入 if current_label.strip() == '0': data_list.append(1) data_list.append(0) data_list.append(0) elif current_label.strip() == '1': data_list.append(0) data_list.append(1) data_list.append(0) else: data_list.append(0) data_list.append(0) data_list.append(1) result_string = str(data_list).replace('[', '').replace(']', '').replace('\'', '').strip() csv_write.writerow(result_string.split(',')) # 关闭输出文件 out_file.close() if __name__ == "__main__": process('D:\\circles.csv')需要注意的是,生成的CSV文件共有15列,前12列为坐标对应的半径值,最后三列组合起来表示分类(label): 图片 (1,0,0)表示类型为“0”的圆,(0,1,0)表示类型为“1”的圆,(0,0,1)表示类型为“2”的圆,这样做的目的是为了下一步使用神经网络时处理起来方便。 5.构建BP神经网络 上一步处理好的数据可以作为训练数据,命名为:circles_data_training.csv 重复第3步和第4步,可以生成另一批数据作为测试数据,命名为:circles_data_test.csv 当然,也可以手动划分出训练数据和测试数据。 训练数据和测试数据在输入时,做了矩阵的转置,将列转置为行。 代码如下:data_analysis_bpnn.py import pandas as pd import numpy as np import datetime from sklearn.utils import shuffle # 1.初始化参数 def initialize_parameters(n_x, n_h, n_y): np.random.seed(2) # 权重和偏置矩阵 w1 = np.random.randn(n_h, n_x) * 0.01 b1 = np.zeros(shape=(n_h, 1)) w2 = np.random.randn(n_y, n_h) * 0.01 b2 = np.zeros(shape=(n_y, 1)) # 通过字典存储参数 parameters = {'w1': w1, 'b1': b1, 'w2': w2, 'b2': b2} return parameters # 2.前向传播 def forward_propagation(X, parameters): w1 = parameters['w1'] b1 = parameters['b1'] w2 = parameters['w2'] b2 = parameters['b2'] # 通过前向传播来计算a2 z1 = np.dot(w1, X) + b1 # 这个地方需注意矩阵加法:虽然(w1*X)和b1的维度不同,但可以相加 a1 = np.tanh(z1) # 使用tanh作为第一层的激活函数 z2 = np.dot(w2, a1) + b2 a2 = 1 / (1 + np.exp(-z2)) # 使用sigmoid作为第二层的激活函数 # 通过字典存储参数 cache = {'z1': z1, 'a1': a1, 'z2': z2, 'a2': a2} return a2, cache # 3.计算代价函数 def compute_cost(a2, Y, parameters): m = Y.shape[1] # Y的列数即为总的样本数 # 采用交叉熵(cross-entropy)作为代价函数 logprobs = np.multiply(np.log(a2), Y) + np.multiply((1 - Y), np.log(1 - a2)) cost = - np.sum(logprobs) / m return cost # 4.反向传播(计算代价函数的导数) def backward_propagation(parameters, cache, X, Y): m = Y.shape[1] w2 = parameters['w2'] a1 = cache['a1'] a2 = cache['a2'] # 反向传播,计算dw1、db1、dw2、db2 dz2 = a2 - Y dw2 = (1 / m) * np.dot(dz2, a1.T) db2 = (1 / m) * np.sum(dz2, axis=1, keepdims=True) dz1 = np.multiply(np.dot(w2.T, dz2), 1 - np.power(a1, 2)) dw1 = (1 / m) * np.dot(dz1, X.T) db1 = (1 / m) * np.sum(dz1, axis=1, keepdims=True) grads = {'dw1': dw1, 'db1': db1, 'dw2': dw2, 'db2': db2} return grads # 5.更新参数 def update_parameters(parameters, grads, learning_rate=0.0075): w1 = parameters['w1'] b1 = parameters['b1'] w2 = parameters['w2'] b2 = parameters['b2'] dw1 = grads['dw1'] db1 = grads['db1'] dw2 = grads['dw2'] db2 = grads['db2'] # 更新参数 w1 = w1 - dw1 * learning_rate b1 = b1 - db1 * learning_rate w2 = w2 - dw2 * learning_rate b2 = b2 - db2 * learning_rate parameters = {'w1': w1, 'b1': b1, 'w2': w2, 'b2': b2} return parameters # 建立神经网络 def nn_model(X, Y, n_h, n_input, n_output, num_iterations=10000, print_cost=False): np.random.seed(3) n_x = n_input # 输入层节点数 n_y = n_output # 输出层节点数 # 1.初始化参数 parameters = initialize_parameters(n_x, n_h, n_y) # 梯度下降循环 for i in range(0, num_iterations): # 2.前向传播 a2, cache = forward_propagation(X, parameters) # 3.计算代价函数 cost = compute_cost(a2, Y, parameters) # 4.反向传播 grads = backward_propagation(parameters, cache, X, Y) # 5.更新参数 parameters = update_parameters(parameters, grads) # 每1000次迭代,输出一次代价函数 if print_cost and i % 1000 == 0: print('迭代第%i次,代价函数为:%f' % (i, cost)) return parameters # 对模型进行测试 def predict(parameters, x_test, y_test): w1 = parameters['w1'] b1 = parameters['b1'] w2 = parameters['w2'] b2 = parameters['b2'] z1 = np.dot(w1, x_test) + b1 a1 = np.tanh(z1) z2 = np.dot(w2, a1) + b2 a2 = 1 / (1 + np.exp(-z2)) # 结果的维度 n_rows = y_test.shape[0] n_cols = y_test.shape[1] # 预测值结果存储 output = np.empty(shape=(n_rows, n_cols), dtype=int) # 取出每条测试数据的预测结果 for i in range(n_cols): # 将每条测试数据的预测结果(概率)存为一个行向量 temp = np.zeros(shape=n_rows) for j in range(n_rows): temp[j] = a2[j][i] # 将每条结果(概率)从小到大排序,并获得相应下标 sorted_dist = np.argsort(temp) length = len(sorted_dist) # 将概率最大的置为1,其它置为0 for k in range(length): if k == sorted_dist[length - 1]: output[k][i] = 1 else: output[k][i] = 0 print('预测结果:') print(output) print('真实结果:') print(y_test) count = 0 for k in range(0, n_cols): if output[0][k] == y_test[0][k] and output[1][k] == y_test[1][k] and output[2][k] == y_test[2][k]: count = count + 1 acc = count / int(y_test.shape[1]) * 100 print('准确率:%.2f%%' % acc) if __name__ == "__main__": # 读取数据 data_set = pd.read_csv('D:\\circles_data_training.csv', header=None) data_set = shuffle(data_set) # 打乱数据的输入顺序 # 取出“特征”和“标签”,并做了转置,将列转置为行 X = data_set.ix[:, 0:11].values.T # 前12列是特征 Y = data_set.ix[:, 12:14].values.T # 后3列是标签 Y = Y.astype('uint8') # 开始训练 start_time = datetime.datetime.now() # 输入12个节点,隐层6个节点,输出3个节点,迭代10000次 parameters = nn_model(X, Y, n_h=6, n_input=12, n_output=3, num_iterations=10000, print_cost=True) end_time = datetime.datetime.now() print("用时:" + str((end_time - start_time).seconds) + 's' + str(round((end_time - start_time).microseconds / 1000)) + 'ms') # 对模型进行测试 data_test = pd.read_csv('D:\\circles_data_test.csv', header=None) x_test = data_test.ix[:, 0:11].values.T y_test = data_test.ix[:, 12:14].values.T y_test = y_test.astype('uint8') predict(parameters, x_test, y_test)码中需要注意的几个关键参数: learning_rate=0.0075,学习率(可调) n_h=6,隐藏层节点数(可调) n_input=12,输入层节点数 n_output=3,输出层节点数 num_iterations=10000,迭代次数(可调) 另外,对于predict(parameters, x_test, y_test)函数需要说明一下: a2矩阵是最终的预测结果,但是是以概率的形式表示的(可以打印看一下)。通过比较3个类的概率,选出概率最大的那个置为1,其它两个置为0,形成output矩阵。 运行结果: 图片 上图中第一红框表示神经网络预测出的分类结果,第二个红框表示测试集中真实的分类((1,0,0)表示这个圆属于类型“0”)。 每次运行时,正确率可能不一样,最高能达到100%。通过调整刚才提到的关键参数中的学习率、隐藏层节点数、迭代次数可以提高正确率。 总结 神经网络的输入为12个半径值,输出结果为一个3维向量,其中置1的位就是对应的分类。 在实际应用中,12个半径值对应12个特征,3维向量表示能分3类。只要根据实际应用的需要修改特征数和分类数即可将上述程序应用于不同分类场景。
机器学习
# 机器学习
刘航宇
3年前
0
487
1
2023-10-15
DL-神经网络的正向传播与反向传播
神经网络简述 前向传播 反向传播 四、深层神经网络(Deep Neural Network) 1.单个神经元 2.由神经元组成的神经网络 3.目标函数 4.求解损失函数对某个权值的梯度 5.反向传播算法Backpropgation 5.2 计算一个梯度 5.3 反向传播误差 6.优化示例 神经网络简述 神经网络,就是在Logistic regression的基础上增加了一个或几个隐层(hidden layer),下面展示的是一个最最最简单的神经网络,只有两层: 两层神经网络 图片 需要注意的是,上面的图是“两层”,而不是三层或者四层,输入和输出不算层! 这里,我们先规定一下记号(Notation): z是x和w、b线性运算的结果,z=wx+b; a是z的激活值; 下标的1,2,3,4代表该层的第i个神经元(unit); 上标的[1],[2]等代表当前是第几层。 y^代表模型的输出,y才是真实值,也就是标签 另外,有一点经常搞混: 上图中的x1,x2,x3,x4不是代表4个样本!而是一个样本的四个特征(4个维度的值)! 你如果有m个样本,代表要把上图的过程重复m次: 图片 神经网络的“两个传播”: 前向传播(Forward Propagation)前向传播就是从input,经过一层层的layer,不断计算每一层的z和a,最后得到输出y^ 的过程,计算出了y^,就可以根据它和真实值y的差别来计算损失(loss)。 反向传播(Backward Propagation)反向传播就是根据损失函数L(y^,y)来反方向地计算每一层的z、a、w、b的偏导数(梯度),从而更新参数。 前向传播和反向传播: 图片 每经过一次前向传播和反向传播之后,参数就更新一次,然后用新的参数再次循环上面的过程。这就是神经网络训练的整个过程。 前向传播 如果用for循环一个样本一个样本的计算,显然太慢,是使用Vectorization,把m个样本压缩成一个向量X来计算,同样的把z、a都进行向量化处理得到Z、A,这样就可以对m的样本同时进行表示和计算了。不熟悉的朋友可以看这里: 这样,我们用公式在表示一下我们的两层神经网络的前向传播过程: Layer 1:Z[1] = W[1]·X + b[1]A[1] = σ(Z[1]) Layer 2:Z[2] = W[2]·A[1] + b[2]A[2] = σ(Z[2]) 而我们知道,X其实就是A[0],所以不难看出:每一层的计算都是一样的: Layer i:Z[i] = W[i]·A[i-1] + b[i]A[i] = σ(Z[i]) (注:σ是sigmoid函数) 因此,其实不管我们神经网络有几层,都是将上面过程的重复。 对于 损失函数,就跟Logistic regression中的一样,使用 “交叉熵(cross-entropy)”,公式就是 二分类问题:L(y^,y) = -[y·log(y^ )+(1-y)·log(1-y^ )]- 多分类问题:L=-Σy(j)·y^(j) 这个是每个样本的loss,我们一般还要计算整个样本集的loss,也称为cost,用J表示,J就是L的平均: J(W,b) = 1/m·ΣL(y^(i),y(i)) 上面的求Z、A、L、J的过程就是正向传播。 反向传播 反向传播说白了根据根据J的公式对W和b求偏导,也就是求梯度。因为我们需要用梯度下降法来对参数进行更新,而更新就需要梯度。 但是,根据求偏导的链式法则我们知道,第l层的参数的梯度,需要通过l+1层的梯度来求得,因此我们求导的过程是“反向”的,这也就是为什么叫“反向传播”。 具体求导的过程,这里就不赘述了。像各种 深度学习框架TensorFlow、Keras,它们都是 只需要我们自己构建正向传播过程, 反向传播的过程是自动完成的,所以大家也确实不用操这个心。 进行了反向传播之后,我们就可以根据每一层的参数的梯度来更新参数了,更新了之后,重复正向、反向传播的过程,就可以不断训练学习更好的参数了。 四、深层神经网络(Deep Neural Network) 前面的讲解都是拿一个两层的很浅的神经网络为例的。 深层神经网络也没什么神秘,就是多了几个/几十个/上百个hidden layers罢了。 可以用一个简单的示意图表示: 深层神经网络: 图片 注意,在深层神经网络中,我们在中间层使用了 “ReLU”激活函数,而不是sigmoid函数了,只有在最后的输出层才使用了sigmoid函数,这是因为 ReLU函数在求梯度的时候更快,还可以一定程度上防止梯度消失现象,因此在深层的网络中常常采用。 关于深层神经网络,我们有必要再详细的观察一下它的结构,尤其是 每一层的各个变量的维度,毕竟我们在搭建模型的时候,维度至关重要。 图片 我们设: 总共有m个样本,问题为二分类问题(即y为0,1); 网络总共有L层,当前层为l层(l=1,2,...,L); 第l层的单元数为n[l];那么下面参数或变量的维度为: W[l]:(n[l],n[l-1])(该层的单元数,上层的单元数) b[l]:(n[l],1) z[l]:(n[l],1) Z[l]:(n[l],m) a[l]:(n[l],1) A[l]:(n[l],m) X:(n[0],m) Y:(1,m) 可能有人问,为什么 W和b的维度里面没有m? 因为 W和b对每个样本都是一样的,所有样本采用同一套参数(W,b), 而Z和A就不一样了,虽然计算时的参数一样,但是样本不一样的话,计算结果也不一样,所以维度中有m。 深度神经网络的正向传播、反向传播和前面写的2层的神经网络类似,就是多了几层,然后中间的激活函数由sigmoid变为ReLU了。 1.单个神经元 神经网络是由一系列神经元组成的模型,每一个神经元实际上做得事情就是实现非线性变换。 如下图就是一个神经元的结构: 图片 神经元将两个部分:上一层的输出(x1,x2,....,xn)与权重(w1,w2,....,wn),对应相乘相加,然后再加上一个偏置 b之后的值经过激活函数处理后完成非线性变换。 记 z = w ⋅ x + b ,a=σ(z),则 z 是神经元非线性变换之前的结果,这部分仅仅是一个简单的线性函数。σ 是Sigmod激活函数,该函数可以将无穷区间内的数映射到(-1,1)的范围内。a 是神经元将 z z 进行非线性变换之后的结果。 Sigmod函数图像如下图 $$\mathrm{sigmod(x)=\frac{1}{1+e^{-x}}}$$ 图片 因此,结果 a 就等于: 图片 这里再强调一遍,神经元的本质就是做非线性变换 2.由神经元组成的神经网络 神经元可以理解成一个函数,神经网络就是由很多个非线性变换的神经元组成的模型,因此神经网络可以理解成是一个非常复杂的复合函数。 图片 对于上图中的网络: (x1,x2,....,xn)为n维输入向量 Wij,表示后一层低i个神经元与前一层第j个神经元之间的权值 z = Wx + b是没有经过激活函数非线性变换之前的结果 a=σ(z),是 z 经过激活函数非线性变换之后的结果 \mathrm{s=U^Ta},为网络最终的输出结果。 3.目标函数 以折页损失为目标函数: $$\mathrm{minmizeJ}=\max(\Delta+s_{c}-s,0)$$ 其中$\mathrm{s_c=U^T\sigma(Wx_c+b),s=U^T\sigma(Wx+b)}$。一般可以把$\Delta$固定下来,比如设为1或者10。 一般来说,学习算法的学习过程就是优化调整参数,使得损失函数,或者说预测结果和实际结果的误差减小。BP算法其实是一个双向算法,包含两个步骤: 1.正向传递输入信息,得到 Loss 值。 2.反向传播误差,从而由优化算法来调整网络权值。 4.求解损失函数对某个权值的梯度 图片 其中$\mathrm{s_c=U^T\sigma(Wx_c+b),s=U^T\sigma(Wx+b)}$。一般可以把$\Delta$固定下来,比如设为1或者10。 对于上面的图, 假设图中指示出的网络中的某个权值 w$_\mathrm{jk}^\mathrm{l}$ 发生了一个小的改变 $\Delta w_\mathrm{jk}^\mathrm{l}$ , 假设网络最终损失函数的输出为$\mathbb{C}$,则 C 应该是关于 $\mathrm{w}_\mathrm{jk}^{\mathrm{l}}$ 的一个复合函数。 所谓复合函数,就是把 $\mathbb{C}$ 看成因变量,则$\mathrm{w}_\mathrm{jk}^{\mathrm{l}}$ 可以看成导致 C 改变的自变量, 比如假设有一个复 合函数 $\mathrm{y=f(g(x))}$,则 y 就好比这里的$\mathbb{C}$ , x 就好比$\mathrm{w}_{\mathrm{jk}}^{\mathrm{l}}$, w$_{jk}^{\mathrm{l}}$ 每经过一层网络可以看成是经过某个函数的处理。而下面求写的时候都用偏导数, 是因为虽然我们这里只关注了一个 w$_\mathrm{jk}^{\mathrm{l}}$,但是实际上网络中的每一个 w 都可以看成一个 x。 显然,这个$\Delta w_\mathrm{jk}^\mathrm{l}$的变化会引起下一层直接与其相连的一个神经元,以及下一层之后所有神经元直到 最终输出 C 的变化, 如图中蓝线标记的就是该权值变化的影响传播路径。 把 C 的改变记为 $\Delta\mathbb{C}$, 则根据高等数学中导数的知识可以得到: 则神经元 a$_\mathrm{j}^1$ 下面一层第 q 个与其相连的神经元 a$_{\mathrm{q}}^{1+1}$ 的变化为: $$ \Delta\mathrm{a_q^{1+1}}\approx\frac{\partial\mathrm{a_q^{1+1}}}{\partial\mathrm{a_j^1}}\Delta\mathrm{a_j^1} $$将 $(2)$ 代入 (3) 可以得到: $$ \Delta\mathrm{a_{q}^{l+1}}\:\approx\:\frac{\partial\mathrm{a_{q}^{l+1}}}{\partial\mathrm{a_{j}^{l}}}\:\frac{\partial\mathrm{a_{j}^{l}}}{\partial\mathrm{w_{jk}^{l}}}\:\Delta\mathrm{w_{jk}^{l}} $$假设从 $\mathrm{a_j}^1$ 到 C的一条路径为$\mathrm{a_j}^1,\mathrm{a_q}^{1+1},...,\mathrm{a_n}^{\mathrm{L-1}},\mathrm{a_m}^{\mathrm{L}}$,则在该条路径上 C 的变化量 $\Delta C$ 为: $$\Delta\mathrm{C}\approx\frac{\partial\mathrm{C}}{\partial\mathrm{a}_\mathrm{m}^\mathrm{L}}\frac{\partial\mathrm{a}_\mathrm{m}^\mathrm{L}}{\partial\mathrm{a}_\mathrm{n}^\mathrm{L-1}}\frac{\partial\mathrm{a}_\mathrm{n}^\mathrm{L-1}}{\partial\mathrm{a}_\mathrm{p}^\mathrm{L-2}}[USD3P]\frac{\partial\mathrm{a}_\mathrm{q}^\mathrm{l+1}}{\partial\mathrm{a}_\mathrm{j}^\mathrm{l}}\frac{\partial\mathrm{a}_\mathrm{j}^\mathrm{l}}{\partial\mathrm{w}_\mathrm{jk}^\mathrm{l}}\Delta\mathrm{w}_\mathrm{jk}^\mathrm{l}$$ 至此,我们已经得到了一条路径上的变化量, 其实本质就是链式求导法则,或者说是复合函数求导法则。那么整个的变化量就县把所有可能链路上的变化量加起来: $$\Delta\mathrm{C}\approx\sum_{\mathrm{minp[USD3P]q}}\frac{\partial\mathrm{C}}{\partial\mathrm{a_{m}^{L}}}\frac{\partial\mathrm{a_{m}^{L}}}{\partial\mathrm{a_{n}^{L-1}}}\frac{\partial\mathrm{a_{n}^{L-1}}}{\partial\mathrm{a_{p}^{L-2}}}[USD3P]\frac{\partial\mathrm{a_{q}^{l+1}}}{\partial\mathrm{a_{j}^{l}}}\frac{\partial\mathrm{a_{j}^{l}}}{\partial\mathrm{w_{jk}^{l}}}\Delta\mathrm{w_{jk}^{l}}$$ $$\begin{aligned}\text{则 C 对某个权值 w}_{\mathrm{jk}}^{\dagger}&\text{的梯度为:}\\&\frac{\partial\mathrm{C}}{\partial\mathrm{w}_{\mathrm{jk}}^{\dagger}}\approx\sum_{\mathrm{nmp},<0}\frac{\partial\mathrm{C}}{\partial\mathrm{a}_{\mathrm{m}}^{\mathrm{L}}}\frac{\partial\mathrm{a}_{\mathrm{m}}^{\mathrm{L}}}{\partial\mathrm{a}_{\mathrm{n}}^{\mathrm{L}-1}}\frac{\partial\mathrm{a}_{\mathrm{n}}^{\mathrm{L}-1}}{\partial\mathrm{a}_{\mathrm{p}}^{\mathrm{L}-2}}[USD3P]\frac{\partial\mathrm{a}_{\mathrm{q}}^{\mathrm{l}+1}}{\partial\mathrm{a}_{\mathrm{j}}^{\mathrm{l}}}\frac{\partial\mathrm{a}_{\mathrm{j}}^{\mathrm{l}}}{\partial\mathrm{w}_{\mathrm{jk}}^{\mathrm{l}}}\end{aligned}$$ 到这里从数学分析的角度来说,我们可以知道这个梯度是可以计算和求解的。 总结: 1.每两个神经元之间是由一条边连接的,这个边就是一个权重值,它是后一个神经元 z 部分,也就是未经激活函数非线性变换之前的结果对前一个神经元的 a 部分,也就是经激活函数非线性变换之后的结果的偏导数。 2.一条链路上所有偏导数的乘积就是这条路径的变化量。 3.所有路径变化量之和就是整个损失函数的变化量。 5.反向传播算法Backpropgation 5.1 明确一些定义 图片 对于上面的神经网络, 首先明确下面一些定义: 图片 5.2 计算一个梯度 假设损失函数为:J=(1+sc −s) 来计第一下 $\frac{\partial\mathrm{J}}{\partial\mathrm{W}_{14}^{(1)}}$,由于 s 是网络的输出, $\frac{\partial\mathrm{J}}{\partial\mathrm{s}}=-1$, 所以只需要计算$\frac{\partial\mathrm{s}}{\partial\mathrm{W}_{14}^{(1)}}$ 即可。 而由于 s 激活函数就是1, 所以有: $$ \mathrm{s=a_1^{(3)}=z_1^{(3)}=W_{11}^{(2)}a_1^{(2)}+W_{12}^{(2)}a_2^{(2)}} $$$$ \frac{\partial\mathrm{s}}{\partial\mathrm{W}_{14}^{(1)}}=\frac{\partial(\mathrm{W}_{11}^{(2)}\mathrm{a}_{1}^{(2)}+\mathrm{W}_{12}^{(2)}\mathrm{a}_{2}^{(2)})}{\partial\mathrm{W}_{14}^{(1)}} $$图片 所以: $$\frac{\partial s}{\partial\mathrm{W}_{14}^{(1)}}=\frac{\partial\mathrm{W}_{11}^{(2)}\mathrm{a}_{1}^{(2)}}{\partial\mathrm{W}_{14}^{(1)}}=\mathrm{W}_{11}^{(2)}\frac{\partial\mathrm{a}_{1}^{(2)}}{\partial\mathrm{W}_{14}^{(1)}}$$ 由于图片代入公式(9)可以得到 $$\begin{gathered} \frac{\partial\mathrm{s}}{\partial\mathrm{W}_{14}^{(1)}}=\mathrm{W}_{11}^{(2)}\frac{\partial\mathrm{a}_{1}^{(2)}}{\partial\mathrm{W}_{14}^{(1)}}=\mathrm{W}_{11}^{(2)}\frac{\partial\mathrm{a}_{1}^{(2)}}{\partial\mathrm{z}_{1}^{(2)}}\frac{\partial\mathrm{z}_{1}^{(2)}}{\partial\mathrm{W}_{14}^{(1)}1} \ =\operatorname{W}_{11}^{(2)}\mathfrak{\sigma}^{\prime}(\mathfrak{z}_{1}^{(2)})\frac{\partial(\mathfrak{b}_{1}^{(1)}+\sum_{\mathrm{k}}\mathfrak{a}_{\mathrm{k}}^{(1)}\mathfrak{W}_{1\text{k}} ^ { ( 1 ) })}{\partial\mathrm{W}_{14}^{(1)}} \end{gathered}$$ $$ \text{公式}(10)中\frac{\partial(\mathrm{h}_{1}^{(1)}+\sum_{k}a_{k}^{(1)}W_{1k}^{(1)})}{\partial\mathrm{W}_{14}^{(1)}}=\mathrm{a}_{4}^{(1)}。 $$此外还有一个更重要的变换, 同样的方法可以求出: $$ \frac{\partial\mathrm{J}}{\partial\mathrm{z}_1^{(2)}}=\mathrm{W}_{11}^{(2)}\mathrm{\sigma}^{\prime}(\mathrm{z}_1^{(2)}) $$$$ \frac{\partial\mathrm{s}}{\partial\mathrm{W}_{14}^{(1)}}=\frac{\partial\mathrm{J}}{\partial\mathrm{z}_1^{(2)}}\mathrm{a}_4^{(1)}=\delta_1^{(2)}\mathrm{a}_4^{(1)} $$并且结合前面对 $\delta_\mathrm{j}^{(\mathrm{k})}$ 的定义,公式 (10) 可以写成: 所以损失函数对任离一个网络权伯的梯度可以弓成两个值相乘的形式。对于$\mathrm{a}_4^{(1)}$,它是网络前向传递过程中的一个神经元的输出, 我们当然可以在网络前向传递的时候将它保存下来。而对于 $\delta_1^{(2)}$,它是反向传播过来的梯度,也就是 J 对 z 的梯度, 下面来者如何通过后面神经元的 $\delta_{\mathrm{i}}^{(\mathrm{k})}$ 反向传播得到 前一个神经元处的 $\delta_\mathrm{j}^{(k-1)}$ 。 5.3 反向传播误差 到这里需要注意,反向传播仅指用于计算梯度的方法,它并不是用于神经网络的整个学习算法。通过反向传播算法算法得到梯度之后,可以使用比如随机梯度下降算法等来进行学习。 反向传播算法传播的是误差,传播方向是从最后一层依次往前,这是一个迭代的过程。在上面的过程中,我们求得了损失函数对于某个权值的梯度,通过该处的梯度值,可以将其向前传播,得到前一个结点的梯度。 图片 $(k-1)$ 例如对于上面的图, 假设已经求出 z$^{(\mathrm{k})}$ 处的柠伊卡$^{(\mathrm{k})}$,则行误差仅照某杀连接路径, 传递到 $\mathrm{a_j}^{[\mathrm{l}]}$ per 处,则该处的梯度为$\delta_{\mathrm{i}}^{(\mathrm{k})}W_{\mathrm{ij}}^{(\mathrm{k}-1)}$。实际上这只是一条路径, $\mathrm{a_j}^{(\mathrm{k-1})}$处可能会收到很多个不同的误差, 例如下面该神经元后面有两条权值 边的情况: 图片 这个时候只要把它们相加就好了,所以 a$_{\mathrm{j}}^{(\mathrm{k}-1)}$处,则该处的梯度为$\sum_{\mathrm{i}}\delta_{\mathrm{i}}^{(\mathrm{k})}\mathrm{W}_{\mathrm{ij}}^{(\mathrm{k}-1)}$。 图片 6.优化示例 再次说一下,反向传播仅指用于计算梯度的方法,它并不是用于神经网络的整个学习算法。通过反向传播算法算法得到梯度之后,还需要使用比如随机梯度下降算法等来进行学习。 在上面的过程中, 我们通过反向传播弹法求解出了任意一个$z_j^{(\mathrm{i})}$处的梯度$\delta_j^{(\mathrm{i})}$,为什么是$z_j^{(\mathrm{i})}$而不是$a_j^{(\mathrm{i})}$,因为$z_j^{(\mathrm{i})}$前面就是连接它的w,以随机梯度下降算法为例,w的优化方法为: $$\mathrm{w_{ijnew}^{(1)}=w_{ijold}^{(1)}-\eta\cdot\frac{\partial J}{\partial w_{ij}^{(1)}}=w_{ijold}^{(1)}-\eta\cdot\delta_{i}^{(1+1)}\cdot a_{j}^{(1)}}$$
机器学习
# DL/ML
刘航宇
3年前
0
673
2
RFID编码简介
信号编码系统包括信源编码和信道编码两大类,器作用是把要传输的信息尽可能的与传输信道相匹配,并提供对信息的某种保护以防止信息受到干扰。信源编码与信源译码的目的是提高信息传输的有效性以及完成模数转换等;信道编码与信道译码的目的是增强信号的抗干扰能力,提高传输的可靠性。 常见的编码方法如下图: 图片 RFID系统常用编码方法: 反向不归零(NRZ)编码 曼彻斯特(Manchester)编码 单极性归零(RZ)编码 差动双相(DBP)编码 密勒(Miller)编码和差动编码 图片 1、反向不归零编码(NRZ,Non Return Zero) 反向不归零编码用高电平表示二进制“1”,低电平表示二进制“0”,如下图所示: 图片 此码型不宜传输,有以下原因 有直流,一般信道难于传输零频附近的频率分量; 接收端判决门限与信号功率有关,不方便使用; 不能直接用来提取位同步信号,因为NRZ中不含有位同步信号频率成分; 要求传输线有一根接地。 注:ISO14443 TYPE B协议中电子标签和阅读器传递数据时均采用NRZ 2、曼彻斯特编码(Manchester) 曼彻斯特编码也被称为分相编码(Split-Phase Coding)。 某比特位的值是由该比特长度内半个比特周期时电平的变化(上升或下降)来表示的,在半个比特周期时的负跳变表示二进制“1”,半个比特周期时的正跳变表示二进制“0”,如下图所示: 图片 曼彻斯特编码的特点 曼彻斯特编码在采用负载波的负载调制或者反向散射调制时,通常用于从电子标签到读写器的数据传输,因为这有利于发现数据传输的错误。这是因为在比特长度内,“没有变化”的状态是不允许的。 当多个标签同时发送的数据位有不同值时,则接收的上升边和下降边互相抵消,导致在整个比特长度内是不间断的负载波信号,由于该状态不允许,所以读写器利用该错误就可以判定碰撞发生的具体位置。 曼彻斯特编码由于跳变都发生在每一个码元中间,接收端可以方便地利用它作为同步时钟。 注: ISO14443 TYPE A协议中电子标签向阅读器传递数据时采用曼彻斯特编码。 ISO18000-6 TYPE B 读写器向电子标签传递数据时采用的是曼彻斯特编码 3、单极性归零编码(Unipolar RZ) 当发码1时发出正电流,但正电流持续的时间短于一个码元的时间宽度,即发出一个窄脉冲 当发码0时,完全不发送电流 单极性归零编码可用来提取位同步信号。 pPbm28I.png图片 4、差动双相编码(DBP) 差动双相编码在半个比特周期中的任意的边沿表示二进制“0”,而没有边沿就是二进制“1”,如下图所示。此外在每个比特周期开始时,电平都要反相。因此,对于接收器来说,位节拍比较容易重建。 pPbmR2t.png图片 5、密勒编码(Miller) 密勒编码在半个比特周期内的任意边沿表示二进制“1”,而经过下一个比特周期中不变的电平表示二进制“0”。一连串的比特周期开始时产生电平交变,如下图所示,因此,对于接收器来说,位节拍也比较容易重建。 pPbmhKf.png图片 pPbm5qS.png图片 6、修正密勒码编码 7、脉冲-间歇编码 对于脉冲—间歇编码来说,在下一脉冲前的暂停持续时间t表示二进制“1”,而下一脉冲前的暂停持续时间2t则表示二进制“0”,如下图所示。 pPbnUoQ.png图片 这种编码方法在电感耦合的射频系统中用于从读写器到电子标签的数据传输,由于脉冲转换时间很短,所以就可以在数据传输过程中保证从读写器的高频场中连续给射频标签供给能量。 8、脉冲位置编码(PPM,Pulse Position Modulation) 脉冲位置编码与上述的脉冲间歇编码类似,不同的是,在脉冲位置编码中,每个数据比特的宽度是一致的。 其中,脉冲在第一个时间段表示“00”,第二个时间段表示“01”, 第三个时间段表示“10”, 第四个时间段表示“11”, 如图所示 pPbnwJs.png图片 注:ISO15693协议中,数据编码采用PPM 9、FM0编码 FM0(即Bi-Phase Space)编码的全称为双相间隔码编码、 工作原理是在一个位窗内采用电平变化来表示逻辑。如果电平从位窗的起始处翻转,则表示逻辑“1”。如果电平除了在位窗的起始处翻转,还在位窗中间翻转则表示逻辑“0”。 pPbn0Wn.png图片 注:ISO18000-6 typeA 由标签向阅读器的数据发送采用FM0编码 10、PIE编码 PIE(Pulse interval encoding)编码的全称为脉冲宽度编码,原理是通过定义脉冲下降沿之间的不同时间宽度来表示数据。 在该标准的规定中,由阅读器发往标签的数据帧由SOF(帧开始信号)、EOF(帧结束信号)、数据0和1组成。在标准中定义了一个名称为“Tari”的时间间隔,也称为基准时间间隔,该时间段为相邻两个脉冲下降沿的时间宽度,持续为25μs。 pPbnBzq.png图片 注:ISO18000-6 typeA 由阅读器向标签的数据发送采用PIE编码 ============================================= 注:选择编码方法的考虑因素 编码方式的选择要考虑电子标签能量的来源 在REID系统中使用的电子标签常常是无源的,而无源标签需要在读写器的通信过程中获得自身的能量供应。为了保证系统的正常工作,信道编码方式必须保证不能中断读写器对电子标签的能量供应。 在RFID系统中,当电子标签是无源标签时,经常要求基带编码在每两个相邻数据位元间具有跳变的特点,这种相邻数据间有跳变的码,不仅可以保证在连续出现“0”时对电子标签的能量供应,而且便于电子标签从接收到的码中提取时钟信息。 编码方式的选择要考虑电子标签的检错的能力 出于保障系统可靠工作的需要,还必须在编码中提供数据一级的校验保护,编码方式应该提供这种功能。可以根据码型的变化来判断是否发生误码或有电子标签冲突发生。 在实际的数据传输中,由于信道中干扰的存在,数据必然会在传输过程中发生错误,这时要求信道编码能够提供一定程度的检测错误的能力。 曼彻斯特编码、差动双向编码、单极性归零编码具有较强的编码检错能力。 编码方式的选择要考虑电子标签时钟的提取 在电子标签芯片中,一般不会有时钟电路,电子标签芯片一般需要在读写器发来的码流中提取时钟。 曼彻斯特编码、密勒编码、差动双向编码容易使电子标签提取时钟。
通信&信息处理
# 通信&射频
# 软件算法
刘航宇
3年前
0
2,574
0
2023-07-27
一张图看懂数字IC设计前后端全流程(DC ICC PT的关系)
关系图 pCvyhuT.png图片 DC综合后用PrimeTime做一遍STA DC的时候,通过SDC定义了很多约束,留下了很多Margin为后端,DC综合的网表是理想的状态。后端会进行Place和Route并进行CTS,这才接近于真实的电路,后端会通过QRC吐出SPEF, SPEF在转成sdf,供PT分析.此时PT分析的已经不是综合之后的网表,PT分析的是经过PR之后且CTS之后的网表。目的就是看在经过后端处理之后时序是否还signoff。前端DC综合的时候,本身也会进行timing分析,有些路径时序不收敛,DC也会报出来,如果确认这是一条真的路径. 这样你就要改RTL了. DC自己都报时序不收敛,后面也都没有做的必要了。
FPGA&ASIC
EDA&虚拟机
# VLSI
# EDA&虚拟机
刘航宇
3年前
0
2,910
1
2023-07-23
Python机器学习- 鸢尾花分类
1、描述 2、code 3、描述 4、code 1、描述 请编写代码实现train_and_predict功能,实现能够根据四个特征对三种类型的鸢尾花进行分类。 train_and_predict函数接收三个参数: train_input_features—二维NumPy数组,其中每个元素都是一个数组,它包含:萼片长度、萼片宽度、花瓣长度和花瓣宽度。 train_outputs—一维NumPy数组,其中每个元素都是一个数字,表示在train_input_features的同一行中描述的鸢尾花种类。0表示鸢尾setosa,1表示versicolor,2代表Iris virginica。 prediction_features—二维NumPy数组,其中每个元素都是一个数组,包含:萼片长度、萼片宽度、花瓣长度和花瓣宽度。 该函数使用train_input_features作为输入数据,使用train_outputs作为预期结果来训练分类器。请使用训练过的分类器来预测prediction_features的标签,并将它们作为可迭代对象返回(如list或numpy.ndarray)。结果中的第n个位置是prediction_features参数的第n行。 2、code # 导入numpy库,用于处理多维数组 import numpy as np # 导入sklearn库中的数据集、模型选择、度量和朴素贝叶斯模块 from sklearn import datasets from sklearn.model_selection import train_test_split from sklearn import metrics from sklearn.naive_bayes import GaussianNB # 定义train_and_predict函数,接收三个参数:训练输入特征、训练输出标签和预测输入特征 def train_and_predict(train_input_features, train_outputs, prediction_features): # 创建一个高斯朴素贝叶斯分类器对象 clf = GaussianNB() # 使用训练输入特征和训练输出标签来训练分类器 clf.fit(train_input_features, train_outputs) # 使用预测输入特征来预测输出标签,并将结果返回 y_pred = clf.predict(prediction_features) return y_pred # 加载鸢尾花数据集,包含150个样本,每个样本有四个特征和一个标签 iris = datasets.load_iris() # 将数据集随机分成训练集和测试集,其中训练集占70%,测试集占30%,并设置随机种子为0 X_train, X_test, y_train, y_test = train_test_split( iris.data, iris.target, test_size=0.3, random_state=0 ) # 调用train_and_predict函数,使用训练集来训练分类器,并使用测试集来预测标签,将结果赋值给y_pred y_pred = train_and_predict(X_train, y_train, X_test) # 如果y_pred不为空,打印预测标签和真实标签的准确率,即正确预测的比例 if y_pred is not None: print(metrics.accuracy_score(y_test, y_pred))3、描述 机器学习库 sklearn 自带鸢尾花分类数据集,分为四个特征和三个类别,其中这三个类别在数据集中分别表示为 0, 1 和 2,请实现 transform_three2two_cate 函数的功能,该函数是一个无参函数,要求将数据集中 label 为 2 的数据进行移除,也就是说仅保留 label 为 0 和为 1 的情况,并且对 label 为 0 和 1 的特征数据进行保留,返回值为 numpy.ndarray 格式的训练特征数据和 label 数据,分别为命名为 new_feat 和 new_label。 然后在此基础上,实现 train_and_evaluate 功能,并使用生成的 new_feat 和 new_label 数据集进行二分类训练,限定机器学习分类器只能从逻辑回归和决策树中进行选择,将训练数据和测试数据按照 8:2 的比例进行分割。 要求输出测试集上的 accuracy_score,同时要求 accuracy_score 要不小于 0.95。 4、code #导入numpy库,它是一个提供了多维数组和矩阵运算等功能的Python库 import numpy as np #导入sklearn库中的datasets模块,它提供了一些内置的数据集 from sklearn import datasets #导入sklearn库中的model_selection模块,它提供了一些用于模型选择和评估的工具,比如划分训练集和测试集 from sklearn.model_selection import train_test_split #导入sklearn库中的preprocessing模块,它提供了一些用于数据预处理的工具,比如归一化 from sklearn.preprocessing import MinMaxScaler #导入sklearn库中的linear_model模块,它提供了一些线性模型,比如逻辑回归 from sklearn.linear_model import LogisticRegression #导入sklearn库中的metrics模块,它提供了一些用于评估模型性能的指标,比如F1分数、ROC曲线面积、准确率等 from sklearn.metrics import f1_score,roc_auc_score,accuracy_score #导入sklearn库中的tree模块,它提供了一些树形模型,比如决策树 from sklearn.tree import DecisionTreeClassifier #定义一个函数transform_three2two_cate,它的作用是将鸢尾花数据集中的三分类问题转化为二分类问题 def transform_three2two_cate(): #从datasets模块中加载鸢尾花数据集,并赋值给data变量 data = datasets.load_iris() #其中data特征数据的key为data,标签数据的key为target #需要取出原来的特征数据和标签数据,移除标签为2的label和特征数据,返回值new_feat为numpy.ndarray格式特征数据,new_label为对应的numpy.ndarray格式label数据 #需要注意特征和标签的顺序一致性,否则数据集将混乱 #code start here #使用numpy库中的where函数找出标签为2的索引,并赋值给index_arr变量 index_arr = np.where(data.target == 2)[0] #使用numpy库中的delete函数删除特征数据中对应索引的行,并赋值给new_feat变量 new_feat = np.delete(data.data, index_arr, 0) #使用numpy库中的delete函数删除标签数据中对应索引的元素,并赋值给new_label变量 new_label = np.delete(data.target, index_arr) #code end here #返回新的特征数据和标签数据 return new_feat,new_label #定义一个函数train_and_evaluate,它的作用是用决策树分类器来训练和评估鸢尾花数据集 def train_and_evaluate(): #调用transform_three2two_cate函数,得到新的特征数据和标签数据,并赋值给data_X和data_Y变量 data_X,data_Y = transform_three2two_cate() #使用train_test_split函数,将数据集划分为训练集和测试集,其中测试集占20%,并赋值给train_x,test_x,train_y,test_y变量 train_x,test_x,train_y,test_y = train_test_split(data_X,data_Y,test_size = 0.2) #已经划分好训练集和测试集,接下来请实现对数据的训练 #code start here #创建一个决策树分类器的实例,并赋值给estimator变量 estimator = DecisionTreeClassifier() #使用fit方法,用训练集的特征和标签来训练决策树分类器 estimator.fit(train_x, train_y) #使用predict方法,用测试集的特征来预测标签,并赋值给y_predict变量 y_predict = estimator.predict(test_x) #code end here #注意模型预测的label需要定义为 y_predict,格式为list或numpy.ndarray #使用accuracy_score函数,计算测试集上的准确率分数,并打印出来 print(accuracy_score(y_predict,test_y)) #如果这个文件是作为主程序运行,则执行以下代码 if __name__ == "__main__": #调用train_and_evaluate函数 train_and_evaluate() #要求执行train_and_evaluate()后输出为: #1、{0,1},代表数据label为0和1 #2、测试集上的准确率分数,要求>0.95
机器学习
# 机器学习
# Python
刘航宇
3年前
0
577
0
算法-反转链表C&Python实现
描述 基础数据结构知识回顾 题解C++篇 题解Python篇 描述 给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。 数据范围: 0≤n≤1000 要求:空间复杂度 O(1) ,时间复杂度 O(n) 。 如当输入链表{1,2,3}时, 经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。 以上转换过程如下图所示: pCqibqO.png图片 基础数据结构知识回顾 空间复杂度 O (1) 表示算法执行所需要的临时空间不随着某个变量 n 的大小而变化,即此算法空间复杂度为一个常量,可表示为 O (1)。例如,下面的代码中,变量 i、j、m 所分配的空间都不随着 n 的变化而变化,因此它的空间复杂度是 O (1)。 int i = 1; int j = 2; ++i; j++; int m = i + j;时间复杂度 O (n) 表示算法执行的时间与 n 成正比,即此算法时间复杂度为线性阶,可表示为 O (n)。例如,下面的代码中,for 循环里面的代码会执行 n 遍,因此它消耗的时间是随着 n 的变化而变化的,因此这类代码都可以用 O (n) 来表示它的时间复杂度。 for (i=1; i<=n; ++i) { j = i; j++; }题解C++篇 可以先用一个vector将单链表的指针都存起来,然后再构造链表。 此方法简单易懂,代码好些。 // 定义一个Solution类 class Solution { public: // 定义一个函数,接收一个链表的头节点指针,返回一个反转后的链表的头节点指针 ListNode* ReverseList(ListNode* pHead) { // 如果头节点指针为空,直接返回空指针 if (!pHead) return nullptr; // 定义一个vector,用于存储链表中的每个节点指针 vector<ListNode*> v; // 遍历链表,将每个节点指针放入vector中 while (pHead) { v.push_back(pHead); pHead = pHead->next; } // 反转vector,也可以逆向遍历 reverse(v.begin(), v.end()); // 取出vector中的第一个元素,作为反转后的链表的头节点指针 ListNode *head = v[0]; // 定义一个当前节点指针,初始化为头节点指针 ListNode *cur = head; // 从第二个元素开始遍历vector,构造反转后的链表 for (int i=1; i<v.size(); ++i) { // 当前节点的下一个指针指向下一个节点 cur->next = v[i]; // 当前节点后移 cur = cur->next; } // 切记最后一个节点的下一个指针指向nullptr cur->next = nullptr; // 返回反转后的链表的头节点指针 return head; } };初始化:3个指针 1)pre指针指向已经反转好的链表的最后一个节点,最开始没有反转,所以指向nullptr 2)cur指针指向待反转链表的第一个节点,最开始第一个节点待反转,所以指向head 3)nex指针指向待反转链表的第二个节点,目的是保存链表,因为cur改变指向后,后面的链表则失效了,所以需要保存 接下来,循环执行以下三个操作 1)nex = cur->next, 保存作用 2)cur->next = pre 未反转链表的第一个节点的下个指针指向已反转链表的最后一个节点 3)pre = cur, cur = nex; 指针后移,操作下一个未反转链表的第一个节点 循环条件,当然是cur != nullptr 循环结束后,cur当然为nullptr,所以返回pre,即为反转后的头结点 这里以1->2->3->4->5 举例: pCqAcsP.png图片 pCqAgqf.png图片 pCqAWdS.png图片 pCqA4iQ.png图片 pCqAIRs.png图片 // 定义一个Solution类 class Solution { public: // 定义一个函数,接收一个链表的头节点指针,返回一个反转后的链表的头节点指针 ListNode* ReverseList(ListNode* pHead) { // 定义一个前驱节点指针,初始化为nullptr ListNode *pre = nullptr; // 定义一个当前节点指针,初始化为头节点指针 ListNode *cur = pHead; // 定义一个后继节点指针,初始化为nullptr ListNode *nex = nullptr; // 遍历链表,反转每个节点的指向 while (cur) { // 记录当前节点的下一个节点 nex = cur->next; // 将当前节点的下一个指针指向前驱节点 cur->next = pre; // 将前驱节点更新为当前节点 pre = cur; // 将当前节点更新为后继节点 cur = nex; } // 返回反转后的链表的头节点指针,即原链表的尾节点指针 return pre; } };题解Python篇 假设 链表为 1->2->3->4->null 空就是链表的尾 obj: 4->3->2->1->null 那么逻辑是 首先设定待反转链表的尾 pre = none head 代表一个动态的表头 逐步取下一次链表的值 然后利用temp保存 head.next 第一次迭代head为1 temp 为2 原始链表中是1->2 现在我们需要翻转 即 令head.next = pre 实现 1->none 但此时链表切断了 变成了 1->none 2->3->4 所以我们要移动指针,另pre = head 也就是pre从none 变成1 下一次即可完成2->1的链接 此外另head = next 也就是说 把指针移动到后面仍然链接的链表上 这样执行下一次循环 则实现 把2->3 转变为 2->1->none 然后再次迭代 直到最后一次 head 变成了none 而pre变成了4 则pre是新的链表的表头 完成翻转 # -*- coding:utf-8 -*- # 定义一个ListNode类,表示链表中的节点 # class ListNode: # def __init__(self, x): # self.val = x # 节点的值 # self.next = None # 节点的下一个指针 # 定义一个Solution类,用于解决问题 class Solution: # 定义一个函数,接收一个链表的头节点,返回一个反转后的链表的头节点 def ReverseList(self, pHead): # write code here pre = None # 定义一个前驱节点,初始化为None head = pHead # 定义一个当前节点,初始化为头节点 while head: # 遍历链表,反转每个节点的指向 temp = head.next # 记录当前节点的下一个节点 head.next = pre # 将当前节点的下一个指针指向前驱节点 pre = head # 将前驱节点更新为当前节点 head = temp # 将当前节点更新为下一个节点 return pre # 返回反转后的链表的头节点,即原链表的尾节点
编程&脚本笔记
软硬件算法
# 软件算法
# C/C++
# Python
刘航宇
3年前
0
372
2
2023-07-21
嵌入式软件-无需排序找数字
题目 示例 解答code1 code2 题目 有1000个整数,每个数字都在1~200之间,数字随机排布。假设不允许你使用任何排序方法将这些整数有序化,你能快速找到从0开始的第450小的数字吗?(从小到大第450位) 示例 输入 - [184, 87, 178, 116, 194, 136, 187, 93, 50, 22, 163, 28, 91, 60, 164, 127, 141, 27, 173, 137, 12, 169, 168, 30, 183, 131, 63, 124, 68, 136, 130, 3, 23, 59, 70, 168, 194, 57, 12, 43, 30, 174, 22, 120, 185, 138, 199, 125, 116, 171, 14, 127, 92, 181, 157, 74, 63, 171, 197, 82, 106, 126, 85, 128, 137, 106, 47, 130, 114, 58, 125, 96, 183, 146, 15, 168, 35, 165, 44, 151, 88, 9, 77, 179, 189, 185, 4, 52, 155, 200, 133, 61, 77, 169, 140, 13, 27, 187, 95, 140, 196, 171, 35, 179, 68, 2, 98, 103, 118, 93, 53, 157, 102, 81, 87, 42, 66, 90, 45, 20, 41, 130, 32, 118, 98, 172, 82, 76, 110, 128, 168, 57, 98, 154, 187, 166, 107, 84, 20, 25, 129, 72, 133, 30, 104, 20, 71, 169, 109, 116, 141, 150, 197, 124, 19, 46, 47, 52, 122, 156, 180, 89, 165, 29, 42, 151, 194, 101, 35, 165, 125, 115, 188, 57, 144, 92, 28, 166, 60, 137, 33, 152, 38, 29, 76, 8, 75, 122, 59, 196, 30, 38, 36, 194, 19, 29, 144, 12, 129, 130, 177, 5, 44, 164, 14, 139, 7, 41, 105, 19, 129, 89, 170, 118, 118, 197, 125, 144, 71, 184, 91, 100, 173, 126, 45, 191, 106, 140, 155, 187, 70, 83, 143, 65, 198, 108, 156, 5, 149, 12, 23, 29, 100, 144, 147, 169, 141, 23, 112, 11, 6, 2, 62, 131, 79, 106, 121, 137, 45, 27, 123, 66, 109, 17, 83, 59, 125, 38, 63, 25, 1, 37, 53, 100, 180, 151, 69, 72, 174, 132, 82, 131, 134, 95, 61, 164, 200, 182, 100, 197, 160, 174, 14, 69, 191, 96, 127, 67, 85, 141, 91, 85, 177, 143, 137, 108, 46, 157, 180, 19, 88, 13, 149, 173, 60, 10, 137, 11, 143, 188, 7, 102, 114, 173, 122, 56, 20, 200, 122, 105, 140, 12, 141, 68, 106, 29, 128, 151, 185, 59, 121, 25, 23, 70, 197, 82, 31, 85, 93, 173, 73, 51, 26, 186, 23, 100, 41, 43, 99, 114, 99, 191, 125, 191, 10, 182, 20, 137, 133, 156, 195, 5, 180, 170, 74, 177, 51, 56, 61, 143, 180, 85, 194, 6, 22, 168, 105, 14, 162, 155, 127, 60, 145, 3, 3, 107, 185, 22, 43, 69, 129, 190, 73, 109, 159, 99, 37, 9, 154, 49, 104, 134, 134, 49, 91, 155, 168, 147, 169, 130, 101, 47, 189, 198, 50, 191, 104, 34, 164, 98, 54, 93, 87, 126, 153, 197, 176, 189, 158, 130, 37, 61, 15, 122, 61, 105, 29, 28, 51, 149, 157, 103, 195, 98, 100, 44, 40, 3, 29, 4, 101, 82, 48, 139, 160, 152, 136, 135, 140, 93, 16, 128, 105, 30, 50, 165, 86, 30, 144, 136, 178, 101, 39, 172, 150, 90, 168, 189, 93, 196, 144, 145, 30, 191, 83, 141, 142, 170, 27, 33, 62, 43, 161, 118, 24, 162, 82, 110, 191, 26, 197, 168, 78, 35, 91, 27, 125, 58, 15, 169, 6, 159, 113, 187, 101, 147, 127, 195, 117, 153, 179, 130, 147, 91, 48, 171, 52, 81, 32, 194, 58, 28, 113, 87, 15, 156, 113, 91, 13, 80, 11, 170, 190, 75, 156, 42, 21, 34, 188, 89, 139, 167, 171, 85, 57, 18, 7, 61, 50, 38, 6, 60, 18, 119, 146, 184, 74, 59, 74, 38, 90, 84, 8, 79, 158, 115, 72, 130, 101, 60, 19, 39, 26, 189, 75, 34, 158, 82, 94, 159, 71, 100, 18, 40, 170, 164, 23, 195, 174, 48, 32, 63, 83, 191, 93, 192, 58, 116, 122, 158, 175, 92, 148, 152, 32, 22, 138, 141, 55, 31, 99, 126, 82, 117, 117, 3, 32, 140, 197, 5, 139, 181, 19, 22, 171, 63, 13, 180, 178, 86, 137, 105, 177, 84, 8, 160, 58, 145, 100, 112, 128, 151, 37, 161, 19, 106, 164, 50, 45, 112, 6, 135, 92, 176, 156, 15, 190, 169, 194, 119, 6, 83, 23, 183, 118, 31, 94, 175, 127, 194, 87, 54, 144, 75, 15, 114, 180, 178, 163, 176, 89, 120, 111, 133, 95, 18, 147, 36, 138, 92, 154, 144, 174, 129, 126, 92, 111, 19, 18, 37, 164, 56, 91, 59, 131, 105, 172, 62, 34, 86, 190, 74, 5, 52, 6, 51, 69, 104, 86, 7, 196, 40, 150, 121, 168, 27, 164, 78, 197, 182, 66, 161, 37, 156, 171, 119, 12, 143, 133, 197, 180, 122, 71, 185, 173, 28, 35, 41, 84, 73, 199, 31, 64, 148, 151, 31, 174, 115, 60, 123, 48, 125, 83, 36, 33, 5, 155, 44, 99, 87, 41, 79, 160, 63, 63, 84, 42, 49, 124, 125, 73, 123, 155, 136, 22, 58, 166, 148, 172, 177, 70, 19, 102, 104, 54, 134, 108, 160, 129, 7, 198, 121, 85, 109, 135, 99, 192, 177, 99, 116, 53, 172, 190, 160, 107, 11, 17, 25, 110, 140, 1, 179, 110, 54, 82, 115, 139, 190, 27, 68, 148, 24, 188, 32, 133, 123, 82, 76, 51, 180, 191, 55, 151, 132, 14, 58, 95, 182, 82, 4, 121, 34, 183, 182, 88, 16, 97, 26, 5, 123, 93, 152, 98, 33, 135, 182, 107, 16, 58, 109, 196, 200, 163, 98, 84, 177, 155, 178, 110, 188, 133, 183, 22, 67, 164, 61, 83, 12, 86, 87, 86, 131, 191, 184, 115, 77, 117, 21, 93, 126, 129, 40, 126, 91, 137, 161, 19, 44, 138, 129, 183, 22, 111, 156, 89, 26, 16, 171, 38, 54, 9, 123, 184, 151, 58, 98, 28, 127, 70, 72, 52, 150, 111, 129, 40, 199, 89, 11, 194, 178, 91, 177, 200, 153, 132, 88, 178, 100, 58, 167, 153, 18, 42, 136, 169, 99, 185, 196, 177, 6, 67, 29, 155, 129, 109, 194, 79, 198, 156, 73, 175, 46, 1, 126, 198, 84, 13, 128, 183, 22] 输出 - 94 解答 解法一:1、不能排序 2、找从0开始的第450位小的数,注意的“从0开始”这句话。[0-450]这个区间总共有451个数,因此我们需要找的是第451位小的数 开始做题---------------------------------------------------------- 可以利用hash表的特性,使用一个201大小的数组,数组的下标为数据的值,数组的值为数据出现的次数。 可以这么理解 key->代表数据,同时也是数组下标 value->代表数据出现的次数 首先给数组元素初始化为0,也就是每个数据出现的次数都是0。 接着使用循环将每个数据出现的次数添加到数组中 再利用循环将出现的次数累加,如果次数累加到450,就说明找到了第450大的数 code1 /* 1、定义一个大小为201的整型数组arr,用来存储每个数在数组numbers中出现的次数。使用memset函数将所有元素初始化为0。 2、定义一个整型变量i,用来作为循环的计数器。初始化为0。 3、使用while循环遍历数组numbers,对于每个数,将其作为arr的下标,将arr对应的元素加一,表示该数出现了一次。同时将i加一,表示下一个数。 4、重新将i赋值为1,表示从第一个数开始计算出现次数之和。 5、定义一个整型变量sum,用来累计前面的数出现的次数之和。初始化为0。 6、使用while循环遍历arr,从下标1开始,对于每个元素,将其加到sum上,然后判断sum是否大于或等于451。如果是,则跳出循环,表示找到了满足条件的数。如果不是,则继续遍历。 7、返回i,表示找到的数。 */ int find(int* numbers, int numbersLen ) { // write code here int arr[201], i=0, sum=0; //定义一个大小为201的整型数组arr,用来存储每个数在数组numbers中出现的次数。定义一个整型变量i,用来作为循环的计数器。定义一个整型变量sum,用来累计前面的数出现的次数之和。 //初始化数组元素 memset(arr,0,sizeof(arr)); //使用memset函数将所有元素初始化为0。 //循环添加每个数据出现的次数 while(i < numbersLen){ //使用while循环遍历数组numbers arr[numbers[i]]++; //对于每个数,将其作为arr的下标,将arr对应的元素加一,表示该数出现了一次。 i++; //同时将i加一,表示下一个数。 } //循环计算次数,当次数超过451次,那就是找到了 i=1; //重新将i赋值为1,表示从第一个数开始计算出现次数之和。 while((sum=sum+arr[i]) < 451){ //使用while循环遍历arr,从下标1开始 i++; //对于每个元素,将其加到sum上,并将i加一。 } return i; //返回i,表示找到的数。 } code2 解法二:因为知道每个数字的大小:1~200,所以无论序列有多少个数字,可以根据一个200行的表,然后统计所有数字出现的频率。 这个思路在硬件设计上常见,即用数字的值代表查表的地址。 /* 1、定义一个大小为201的整型数组table,用来存储每个数在数组numbers中出现的次数。初始化为0。 2、遍历数组numbers,对于每个数,将其作为table的下标,将table对应的元素加一,表示该数出现了一次。 3、定义一个整型变量acc,用来累计前面的数出现的次数之和。初始化为0。 4、遍历table,从下标1开始,对于每个元素,将其加到acc上,然后判断acc是否大于或等于451。如果是,则返回当前的下标,表示找到了满足条件的数。如果不是,则继续遍历。 5、如果遍历完table都没有找到满足条件的数,则返回0。 */ /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param numbers int整型一维数组 * @param numbersLen int numbers数组长度 * @return int整型 */ int find(int* numbers, int numbersLen ) { // write code here int table[201] = {0}; //定义一个大小为201的整型数组table,用来存储每个数在数组numbers中出现的次数。初始化为0。 for (int i = 0; i < numbersLen; i++) { table[numbers[i]]++; //遍历数组numbers,对于每个数,将其作为table的下标,将table对应的元素加一,表示该数出现了一次。 } int acc = 0; //定义一个整型变量acc,用来累计前面的数出现的次数之和。初始化为0。 for (int i = 1; i < 201; i++) { acc += table[i]; //遍历table,从下标1开始,对于每个元素,将其加到acc上。 if (acc >= 451) return i; //判断acc是否大于或等于451。如果是,则返回当前的下标,表示找到了满足条件的数。 } return 0; //如果遍历完table都没有找到满足条件的数,则返回0。 }
嵌入式&系统
编程&脚本笔记
# 嵌入式
# C/C++
刘航宇
3年前
0
397
0
上一页
1
2
3
4
...
26
下一页