生成器_正则

多线程特点_随机性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import threading


# 使用多线程来模拟小明一边编写num行代码,一边听count首音乐功能实现。
def coding(name, num):
for i in range(num):
print(f"{name}正在敲{i}行代码")


def music(name, count):
for i in range(count):
print(f"{name}正在听{i}遍音乐")


# args 元组
# kwargs 字典
if __name__ == '__main__':
t1 = threading.Thread(target=coding, args=("小明", 100))
t2 = threading.Thread(target=music, args=("小红", 100))
t1.start()
t2.start()

多线程特点_守护线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#
# 假如创建一个子线程,这个子线程执行完大概需要2.5秒钟,
# 现在让主线程执行1秒钟就退出程序,查看一下执行结果
import time, threading
def work():
for i in range(20):
time.sleep(0.25)
print(f"{i}次工作中")
if __name__ == '__main__':
# 方式一:创建子线程的时候指定守护线程daemon=True
# t1 = threading.Thread(target=work,daemon=True)
t1 = threading.Thread(target=work, daemon=True)
t1.setDaemon(True)
t1.start()
time.sleep(1)
print("主线程结束了!!!")

多线程特点_数据共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义一个列表类型的全局变量,创建两个子线程分别执行
# 向全局变量添加数据的任务和向全局变量读取数据的任务,
# 查看线程之间是否共享全局变量数据
import threading
import time
my_list = []
# 添加数据
def write_data():
for i in range(10):
time.sleep(0.5)
my_list.append(i)
print("写入数据", i)
print(f"调用write_data{my_list}")
# 读取数据
def read_data():
time.sleep(1)
print(f"调用read_data{my_list}")
if __name__ == '__main__':
t1 = threading.Thread(target=write_data)
t2 = threading.Thread(target=read_data)
t1.start()
t2.start()

多线程特点_互斥锁

  • 图解

    1742990301370

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import threading

    global_num = 0
    # 创建了一个锁
    lk = threading.Lock()
    def fun1():
    #加锁
    lk.acquire()
    global global_num
    for i in range(10000000):
    global_num += 1
    print(f"fun1的函数结果{global_num}")
    #释放锁
    # lk.release()
    def fun2():
    # 加锁
    lk.acquire()
    global global_num
    for i in range(10000000):
    global_num += 1
    print(f"fun2的函数结果{global_num}")
    lk.release()
    # 释放锁
    if __name__ == '__main__':
    t1 = threading.Thread(target=fun1)
    t2 = threading.Thread(target=fun2)
    t2.start()
    t1.start()


进程和线程对比

1
2
3
4
进程和线程的区别:
1. 线程依赖进程, 进程是CPU分配资源的基本单位, 线程是CPU调度资源的基本单位.
2. 进程更消耗资源, 不能共享全局变量, 相对更稳定.
3. 线程更轻量级, 可以共享全局变量, 相对更灵活.

迭代器入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
"""
迭代器介绍
概述:
自定义迭代器,只要重写 __iter__() 和__next()__方法,就可以称之为迭代器
目的:
隐藏底层的逻辑,让用户使用方便
惰性加载,用的时候用才会获取
回顾:
for i in 可迭代类型
"""
# 需求 : 模拟range(1,6) ,自定义迭代器
# for i in range(6, 1):
# print(i)


# 场景2:自定义迭代器
class my_class:
# range(1, 6): 参考别人的迭代器 。起始 结束
def __init__(self, start, end):
self.current_value = start
self.end = end

# # 返回当前对象(即:迭代器对象)
def __iter__(self):
return self

# 返回当前值,并更新当前值
def __next__(self):
# 判断当前值不合法
if self.current_value >= self.end:
raise StopIteration # 抛出异常,迭代结束
# 走到这里,说明当前值合法。返回当前值,并且更新当前值
value = self.current_value
self.current_value += 1
return value
# self.current_value+=1
# return self.current_value-1
# 使用场景:
for i in my_class(1, 6):

print(i)

生成器介绍

  • 案例1: 推导式写法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    def my_fun():
    for i in range(1, 11):
    # my_list =[] #创建
    # for i in range(1,11):
    # my_list.append(i) #添加
    # return my_list #返回

    #等价于
    # yield 在这里做了三件事
    # 1:创建生成器对象
    # 2:把值存储在生成器中
    # 3:返回生成器
    yield i

    #1:创建生成器对象
    my_g1 = my_fun()
    print(type(my_g1))#<class 'generator'>
    print(next(my_g1))
    print(next(my_g1))
    print("*"*24)
    for i in my_g1:
    print(i)
    print("*")
  • 案例2: yield关键字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    """
    案例: 演示生成器之 推导式写法.

    生成器介绍:
    概述:
    所谓的生成器就是基于 数据规则, 用一部分在生成一部分, 而不是一下子生成完所有.
    目的:
    可以节省大量的内存.
    实现方式:
    1. 推导式写法.
    2. yield关键字
    """

    # 需求: 通过yield方式, 获取到生成器之 1 ~ 10之间的整数.
    # 回顾: 推导式写法.
    my_g = (i for i in range(1, 11))

    # yield方式如下.
    # 1.定义函数, 存储到生成器中, 并返回.
    def my_fun():
    # my_list = [] # 创建
    # for i in range(1, 11):
    # my_list.append(i) # 添加
    # return my_list # 返回

    # 效果类似于上边的代码.
    # yield在这里做了三件事儿: 1.创建生成器对象. 2.把值存储到生成器中. 3.返回生成器.
    for i in range(1, 11):
    yield i

    # 2.测试.
    my_g2 = my_fun()
    print(type(my_g2)) # <class 'generator'>

    print(next(my_g2))
    print(next(my_g2))
    print('-' * 23)
    for i in my_g2:
    print(i)
  • 案例3: 批量歌词

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    """
    需求:基于传入的文件,创建生成器 ,生产批次歌词
    分批加载数据: 歌词 假设 8条为一个批次
    """
    import math
    # 定义函数 返回生成器
    # batch_size 批次大小
    def dataset_loader(batch_size):
    # 读取文件
    with open("./data/jaychou_lyrics.txt", "r", encoding="utf-8") as src_f:
    # 一次读取一行数据
    lines = src_f.readlines()
    # 计算批次总数 ,假设 5批 每个批次8条 5*8 = 40条数据
    # 之所以使用math.ceil,是因为考虑到可能最后歌词数不够8条,也算一个批次
    # 总批次(total_batch) =celi(len(lines) /batch_size)
    total_batch = math.ceil(len(lines) / batch_size)

    # 循环 获取到【每个批次】的数据,放入生成器中,并返回
    for idx in range(total_batch):
    # 如何按照我们的要求来取批次
    # 假设total_batch 是5个批次 range(5) 则idx值 0 1 2 3 4
    # 第一批歌词,批次索引(idx=0),歌词为:第1条~第8条 索引为0~7
    # 第二批格式,批次索引(idx=1),歌词为:第9条~第16条 索引8~15
    # 第三批格式,批次索引(idx=2),歌词为:第17条~24条 索引:16~23
    # yield lines[0:8] 包左不包右 前面的值 idx * batch_size
    # yield lines[8:16] 包左不包右 前面的值 idx *batch_size
    yield lines[idx * batch_size:idx * batch_size + batch_size]
    d1=dataset_loader(8)
    # print(next(d1))

    for tmp_batch in d1:
    print(tmp_batch)

Property属性介绍

  • 场景1: 装饰器用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    """
    property属性介绍:
    概述/目的/作用:
    把函数当做变量来使用
    实现方式:
    方式一:装饰器
    方式二:类属性
    实现方式一:
    property的装饰器用法
    @property 修饰 获取值的函数 get_age
    @获取值的函数名.setter 修饰 设置值的函数 set_age
    """
    # 没有property属性
    class student:
    # 私有化属性 age
    def __init__(self):
    self.__age = 18
    # 没有property属性的时候:
    # 提供公共的访问接口 (get/set)
    @property
    def age(self):
    return self.__age

    @age.setter
    def age(self, age):
    self.__age = age
    if __name__ == '__main__':
    s1 = student()
    # print(s1.get_age())
    # print(s1.get_age)
    print(s1.age)
    s1.age=20
    print(s1.age)
  • 场景2: 类属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    """
    property属性介绍:
    概述/目的/作用:
    把函数当做变量来使用
    实现方式:
    方式一:装饰器
    方式二:类属性
    实现方式一:
    property的装饰器用法
    @property 修饰 获取值的函数 get_age
    @获取值的函数名.setter 修饰 设置值的函数 set_age
    """


    # 没有property属性
    class student:
    # 私有化属性 age
    def __init__(self):
    self.__age = 18

    # 没有property属性的时候:
    # 提供公共的访问接口 (get/set)

    def get_age(self):
    return self.__age

    def set_age(self, age):
    self.__age = age

    age = property(get_age, set_age)


    if __name__ == '__main__':
    s1 = student()
    # print(s1.get_age())
    # print(s1.get_age)
    print(s1.age)
    s1.age = 20
    print(s1.age)

正则表达式入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
"""

re使用方法:
1:导包
import re
2:正则匹配
result=re.match('正则表达式',‘要校验的字符串’) 从前向后依次匹配。只要能匹配上即可
result=re.search('正则表达式',‘要校验的字符串’) 分段查找
3:获取匹配结果
result.group()
细节:
1:学正则表达式,就是学习正则表达式的规则,你不用背。网上一大堆
2:关于正则对大家的要求是,能用我们讲的规则,看懂别人的表达式,且会简单的修改即可
3:正则不独属于python像sql、java、php、go 等都支持
"""
import re
# . 匹配任意1个字符
#参数一 匹配规则
# 参数二 要匹配字符串
# result = re.match('.it','ait') #匹配成功
# result = re.match('.it','你it') #匹配成功、
# result = re.match('.it','你好it') #匹配失败
# result = re.match('.it','@it') #匹配成功

# [] 匹配[]中列举的字符
# result = re.match('[abc]it','ait')
# result = re.match('[abc]it','hit')
# result = re.match('[abc]it','a it') #匹配失败

# [^] 匹配[]中列举的字符
# result = re.match('[^abc]it','ait') #匹配失败
# result = re.match('[^abc]it','a it')匹配失败

#[3-7] 从3个开始到7
# result = re.match('[3-7]it','7it')
# result = re.match('[a-zA-Z0-9]it','_it')

#\d 匹配数字0-9
# result = re.match('a\\dit','a-8it') #a8it

#\D 即匹配的不是数字
# result = re.match('a\\Dit','a-it')

# \s 匹配空白 即空格 tab键
# result = re.match('a\sit','a it')
# result = re.match('a\sit','a it')#匹配失败
# result = re.match('a\sit','a\tit') #匹配成功

# \S 匹配非空白
# result = re.match('a\Sit','a_it') #匹配成功

# \w
# result = re.match('a[\w]it','a_it')

# result = re.match('a[\W]it','a!it')
# result = re.match('a[\W]it','a*it')


# result = re.match("itcast.", "itcast2fdafdafdas")
result = re.match("a[^abc]it","a it")
if result:
print(result.group())
else:
print("匹配失败")




正则表达式_校验单个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
"""
案例: 演示正则表达式之 校验单个字符.

正则表达式介绍:
概述:
正确的, 符合特定规则的 字符串.
Regular Expression, 正则表达式, 简称: re
细节:
1. 学正则表达式, 就是学正则表达式的规则, 你用不背, 网上一搜一大堆.
2. 关于正则我对大家的要求是, 能用我们讲的规则, 看懂别人写的式子, 且会简单修改即可.
3. 正则不独属于Python, 像Java, JavaScript, PHP, Go等都支持.
步骤:
1. 导包
import re
2. 正则匹配
result = re.match('正则表达式', '要校验的字符串') 从前往后依次匹配,只要能匹配即可.
result = re.search('正则表达式', '要校验的字符串') 分段查找.
3. 获取匹配结果.
result.group()
正则常用的规则:
. 代表任意的 1个字符, 除了 \n
\. 取消.的特殊含义, 就是1个普通的.
a 代表1个普通的字符 a
[abc] 代表a,b,c中任意的1个字符
[^abc] 代表除了a,b,c外, 任意的1个字符
\d 代表数字, 等价于 [0-9]
\D 代表非数字, 等价于 [^0-9]
\s 代表空白字符, 等价于 [\t\n\r]
\S 代表非空白字符
\w 代表非特殊字符, 即: 数字, 字母, 下划线, 汉字, [a-zA-Z0-9_汉字]
\W 代表特殊字符, 非字母,数字,下划线,汉字

^
$

*
?
+
{n}
{n,}
{n,m}

| 代表 或者的意思
()
\num

扩展:
(?P<分组名>)
(?P=分组名)
"""

# 需求: 正则入门.

# 1.导包
import re

# 2.正则校验, 参1: 正则规则, 参2: 要被校验的字符串
# result = re.match('.it', 'ait') # 匹配成功
# result = re.match('.it', '你it') # 匹配成功
# result = re.match('.it', '你好it') # 失败

# result = re.match('\.it', '你it') # 失败
# result = re.match('\.it', '.it') # 匹配成功

result = re.match('[ahg]it', 'ait') # 匹配成功
result = re.match('[ahg]it', 'hit') # 匹配成功
result = re.match('[ahg]it', 'git') # 匹配成功
result = re.match('[ahg]it', 'g it') # 失败


result = re.match('[^ahg]it', 'ait') # 失败
result = re.match('[^ahg]it', 'x it') # 失败
result = re.match('[^ahg]it', 'xit') # 匹配成功
result = re.match('[^ahg]it', 'xitabcxyz') # 匹配成功, 从前往后匹配, 匹配到就返回.
result = re.match('[^ahg]it', 'abcxitabcxyz') # 失败, 从前往后依次查找.
# result = re.search('[^ahg]it', 'abcxitabcxyz') # 失败, 从前往后依次查找.


result = re.match('[3-7]it', '3it') # 匹配成功
result = re.match('[3-7]it', '-it') # 失败, [3-7]等价于[34567]


result = re.match('a\\dhm', 'a1hm') # 匹配成功
result = re.match('a\\dhm', 'a10hm') # 失败

result = re.match('a\\Dhm', 'a!hm') # 匹配成功
result = re.match('a\\Dhm', 'abhm') # 匹配成功


result = re.match('a\\shm', 'abhm') # 失败
result = re.match('a\\shm', 'a\thm') # 匹配成功
result = re.match('a\\shm', 'a\nhm') # 匹配成功
result = re.match('a\\shm', 'a hm') # 匹配成功

result = re.match('a\\whm', 'a\thm') # 失败
result = re.match('a\\whm', 'a!hm') # 失败
result = re.match('a\\whm', 'axhm') # 匹配成功
result = re.match('a\\whm', 'a_hm') # 匹配成功
result = re.match('a\\whm', 'a6hm') # 匹配成功
result = re.match('a\\whm', 'aYhm') # 匹配成功
result = re.match('a\\whm', 'a夯hm') # 匹配成功

# 3.获取匹配结果.
if result:
print(result.group())
else:
print('匹配失败')

正则表达式_校验多个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import re

# *匹配前一个字符出现0次或者无限次,即可有可无
# result = re.match("hm.*","abchm123")


# + 匹配前一个字符出现1次或者无限次
# result = re.match(".+hm.*","abchm")
result = re.match(".+hm.*", "hm123")

# ? 匹配前一个字符出现1次或者0次
# result = re.match(".?hm.*","abchm123")
# result = re.match(".?hm.*","ahm123")

# {m} 匹配前一个字符出现m次
# result = re.match("\d{3}hm\w{2,5}","123hm123")
result = re.match("\d{3}hm\w{2,5}", "1234hm123") # 匹配失败
result = re.match("\d{3,}hm\w{2,5}", "123hmabc")

if result:
print(result.group())
else:
print("匹配失败")
import re

# *匹配前一个字符出现0次或者无限次,即可有可无
# result = re.match("hm.*","abchm123")


# + 匹配前一个字符出现1次或者无限次
# result = re.match(".+hm.*","abchm")
result = re.match(".+hm.*", "hm123")

# ? 匹配前一个字符出现1次或者0次
# result = re.match(".?hm.*","abchm123")
# result = re.match(".?hm.*","ahm123")

# {m} 匹配前一个字符出现m次
# result = re.match("\d{3}hm\w{2,5}","123hm123")
result = re.match("\d{3}hm\w{2,5}", "1234hm123") # 匹配失败
result = re.match("\d{3,}hm\w{2,5}", "123hmabc")

if result:
print(result.group())
else:
print("匹配失败")

正则表达式_校验开头和结尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import re
# result = re.match("^\d+.", "qabc123xyz") #失败
# result = re.match("\d+.", "123xyz")

# result = re.match("^\d+.*[a-zA-Z]{3}", "abc123xyz12")
# result = re.match("^\d+.*[a-zA-Z]{3}", "123你好xyz12")

# result = re.match("^\d+.*[a-zA-Z]{3}$", "123你好abc12")

# result = re.match("^\d+.*[a-zA-Z]{3}$", "123你好abc12a&c")
# if result:
# print(result.group())
# else:
# print("匹配失败")


# 校验手机号
# 规则1: 长度必须是11位
# 规则2:第1位必须是1
# 规则3:必须都是纯数字
# 规则4: 第二位数字 3-9

result = re.match("^1[3-9]\d{9}$", "13345678921111")
if result:
print(result.group())
else:
print("匹配失败")

正则表达式_校验分组

1
2
3
4
5
6
7
8
9
# 1 需求:在列表中["apple", "banana", "orange", "pear"],匹配apple和pearfruit = ["apple", "banana", "orange", "pear"]
#
import re
f = ["apple", "banana", "orange", "pear"]
for f_tmp in f:
if re.match('apple|pear',f_tmp):
print(f"喜欢吃{f_tmp}")
else:
print(f"不喜欢吃{f_tmp}")

正则表达式_校验邮箱

1
2
3
4
5
6
7
8
9
10
 # 2 需求:匹配出163、126、qq等邮箱
email ="axcdsa@163.com"
#校验邮箱合法
result = re.match("^[a-zA-Z_0-9]{6,18}@(163|126|qq)\.com$", email)
if result:
print(result.group())
print(result.group(0))#获取第0组信息,效果同上,即;整个匹配到的结果。整个匹配到的结果是0组
print(result.group(1)) #从左往右数,第几个左小括号,就表示第几组
else:
print("匹配失败")

正则表达式_提取QQ号

1
2
3
4
5
6
7
8
9
10
11
12
13
#需求是匹配QQ号
# 数字 qq:10567
import re
result = re.match("^(qq):([1-9]\d{4,10})$", "qq:10567")
if result:
print(result.group())
print(result.group(0))
print(result.group(1))
print(result.group(2))
else:
print("匹配失败")


正则表达式_校验html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
"""
正则校验:
| 代表 或的意思
() 代表分组 ,从左向右数, 第几个左小括号(,就表示第几组
\num 代表引用第几组的内容

拓展:
(?P<分组名>) 设置分组
(?P=分组名) 使用分组

"""
# 需求一:匹配出 <html>hh</html>

import re
# html_s='<html>hh</html>'
# # result = re.match("<[a-zA-Z]{1,4}>.*</[a-zA-Z]{1,4}>", html_s)
#
# result = re.match(r"<([a-zA-Z]{1,4})>.*</\1>", html_s)
# result = re.match("<([a-zA-Z]{1,4})>.*</\\1>", html_s)
# #引入分组的概念
# if result:
# print(result.group())
# else:
# print("匹配失败")



html_s='<html><h1>我是html界面</h1></html>'

# result = re.match("<[a-zA-Z]{1,4}><h[1-6]>.*</h[1-6]></[a-zA-Z]{1,4}>", html_s)

#演示分组
# result = re.match(r"<([a-zA-Z]{1,4})><(h[1-6])>.*</\2></\1>", html_s)


result = re.match("<(?P<A>[a-zA-Z]{1,4})><(?P<B>h[1-6])>.*</(?P=B)></(?P=A)>", html_s)

#引入分组的概念
if result:
print(result.group())
else:
print("匹配失败")

朴素贝叶斯

朴素贝叶斯

朴素贝叶斯介绍

  1. 复习常见概率的计算
  2. 知道贝叶斯公式
  3. 了解朴素贝叶斯是什么
  4. 了解拉普拉斯平滑系数的作用

【知道】常见的概率公式

条件概率: 表示事件A在另外一个事件B已经发生条件下的发生概率,P(A|B)

在女神喜欢的条件下,职业是程序员的概率?

  1. 女神喜欢条件下,有 2、3、4、7 共 4 个样本
  2. 4 个样本中,有程序员 3、4 共 2 个样本
  3. 则 P(程序员|喜欢) = 2/4 = 0.5

联合概率: 表示多个条件同时成立的概率,P(AB) = P(A) P(B|A)
特征条件独立性假设:P(AB) = P(A) P(B)

职业是程序员并且体型匀称的概率?

  1. 数据集中,共有 7 个样本
  2. 职业是程序员有 1、3、4 共 3 个样本,则其概率为:3/7
  3. 在职业是程序员,体型是匀称有 3 共 1 个样本,则其概率为:1/3
  4. 则即是程序员又体型匀称的概率为:3/7 * 1/3 = 1/7

联合概率 + 条件概率:

在女神喜欢的条件下,职业是程序员、体重超重的概率? P(AB|C) = P(A|C) P(B|AC)

  1. 在女神喜欢的条件下,有 2、3、4、7 共 4 个样本
  2. 在这 4 个样本中,职业是程序员有 3、4 共 2 个样本,则其概率为:2/4=0.5
  3. 在在 2 个样本中,体型超重的有 4 共 1 个样本,则其概率为:1/2 = 0.5
  4. 则 P(程序员, 超重|喜欢) = 0.5 * 0.5 = 0.25

简言之:
条件概率:在去掉部分样本的情况下,计算某些样本的出现的概率,表示为:P(B|A)
联合概率:多个事件同时发生的概率是多少,表示为:P(AB) = P(B)*P(A|B)

【理解】贝叶斯公式

  1. P(C) 表示 C 出现的概率
  2. P(W|C) 表示 C 条件 W 出现的概率
  3. P(W) 表示 W 出现的概率
  1. P(C|W) = P(喜欢|程序员,超重)
  2. P(W|C) = P(程序员,超重|喜欢)
  3. P(C) = P(喜欢)
  4. P(W) = P(程序员,超重)

  1. 根据训练样本估计先验概率P(C):P(喜欢) = 4/7
  2. 根据条件概率P(W|C)调整先验概率:P(程序员,超重|喜欢) = 1/4
  3. 此时我们的后验概率P(C|W)为:P(程序员,超重|喜欢) * P(喜欢) = 4/7 * 1/4 = 1/7
  4. 那么该部分数据占所有既为程序员,又超重的人中的比例是多少呢?
    1. P(程序员,超重) = P(程序员) * P(超重|程序员) = 3/7 * 2/3 = 2/7
    2. P(喜欢|程序员, 超重) = 1/7 ➗ 2/7 = 0.5

【理解】朴素贝叶斯

我们发现,在前面的贝叶斯概率计算过程中,需要计算 P(程序员,超重|喜欢) 和 P(程序员, 超重) 等联合概率,为了简化联合概率的计算,朴素贝叶斯在贝叶斯基础上增加:特征条件独立假设,即:特征之间是互为独立的。

此时,联合概率的计算即可简化为:

  1. P(程序员,超重|喜欢) = P(程序员|喜欢) * P(超重|喜欢)
  2. P(程序员,超重) = P(程序员) * P(超重)

【知道】拉普拉斯平滑系数

由于训练样本的不足,导致概率计算时出现 0 的情况。为了解决这个问题,我们引入了拉普拉斯平滑系数。

  1. α 是拉普拉斯平滑系数,一般指定为 1
  2. Ni 是 F1 中符合条件 C 的样本数量
  3. N 是在条件 C 下所有样本的总数
  4. m 表示所有独立样本的总数

我们只需要知道为了避免概率值为 0,我们在分子和分母分别加上一个数值,这就是拉普拉斯平滑系数的作用。

【案例】情感分析

学习目标:

1.知道朴素贝叶斯的API

2.能够应用朴素贝叶斯实现商品评论情感分析

【知道】api介绍

  • sklearn.naive_bayes.MultinomialNB(alpha = 1.0)
    • 朴素贝叶斯分类
    • alpha:拉普拉斯平滑系数

【实践】商品评论情感分析

已知商品评论数据,根据数据进行情感分类(好评、差评

image-20230906224506412

步骤分析

  • 1)获取数据
  • 2)数据基本处理
    • 2.1) 取出内容列,对数据进行分析
    • 2.2) 判定评判标准
    • 2.3) 选择停用词
    • 2.4) 把内容处理,转化成标准格式
    • 2.5) 统计词的个数
    • 2.6)准备训练集和测试集
  • 3)模型训练
  • 4)模型评估

代码实现

1
2
3
4
5
6
import pandas as pd
import numpy as np
import jieba
import matplotlib.pyplot as plt
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
  • 1)获取数据
1
2
3
# 加载数据
data = pd.read_csv("./data/书籍评价.csv", encoding="gbk")
data
  • 2)数据基本处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 2.1) 取出内容列,对数据进行分析
content = data["内容"]
content.head()

# 2.2) 判定评判标准 -- 1好评;0差评
data.loc[data.loc[:, '评价'] == "好评", "评论标号"] = 1 # 把好评修改为1
data.loc[data.loc[:, '评价'] == '差评', '评论标号'] = 0

# data.head()
good_or_bad = data['评价'].values # 获取数据
print(good_or_bad)
# ['好评' '好评' '好评' '好评' '差评' '差评' '差评' '差评' '差评' '好评' '差评' '差评' '差评']

# 2.3) 选择停用词
# 加载停用词
stopwords=[]
with open('./data/stopwords.txt','r',encoding='utf-8') as f:
lines=f.readlines()
print(lines)
for tmp in lines:
line=tmp.strip()
print(line)
stopwords.append(line)
# stopwords # 查看新产生列表

#对停用词表进行去重
stopwords=list(set(stopwords))#去重 列表形式
print(stopwords)

# 2.4) 把“内容”处理,转化成标准格式
comment_list = []
for tmp in content:
print(tmp)
# 对文本数据进行切割
# cut_all 参数默认为 False,所有使用 cut 方法时默认为精确模式
seg_list = jieba.cut(tmp, cut_all=False)
print(seg_list) # <generator object Tokenizer.cut at 0x0000000007CF7DB0>
seg_str = ','.join(seg_list) # 拼接字符串
print(seg_str)
comment_list.append(seg_str) # 目的是转化成列表形式
# print(comment_list) # 查看comment_list列表。

# 2.5) 统计词的个数
# 进行统计词个数
# 实例化对象
# CountVectorizer 类会将文本中的词语转换为词频矩阵
con = CountVectorizer(stop_words=stopwords)
# 进行词数统计
X = con.fit_transform(comment_list) # 它通过 fit_transform 函数计算各个词语出现的次数
name = con.get_feature_names() # 通过 get_feature_names()可获取词袋中所有文本的关键字
print(X.toarray()) # 通过 toarray()可看到词频矩阵的结果
print(name)

# 2.6)准备训练集和测试集
# 准备训练集 这里将文本前10行当做训练集 后3行当做测试集
x_train = X.toarray()[:10, :]
y_train = good_or_bad[:10]
# 准备测试集
x_text = X.toarray()[10:, :]
y_text = good_or_bad[10:]
  • 3)模型训练
1
2
3
4
5
6
7
8
9
# 构建贝叶斯算法分类器
mb = MultinomialNB(alpha=1) # alpha 为可选项,默认 1.0,添加拉普拉修/Lidstone 平滑参数
# 训练数据
mb.fit(x_train, y_train)
# 预测数据
y_predict = mb.predict(x_text)
#预测值与真实值展示
print('预测值:',y_predict)
print('真实值:',y_text)
  • 4)模型评估
1
mb.score(x_text, y_text)

作业

1.使用思维导图整理朴素贝叶斯的内容

2.动手实现商品评论情感分析案例

Python编程_文件与异常

大纲

  • 文件的概念与基本操作

  • Python异常处理方式【掌握】

  • 内置模块与自定义模块的应用【活学活用】

  • 学生管理系统

文件的概念

学习目标

  • 理解文件的相关概述
  • 理解文件的作用

什么是文件

内存中存放的数据在计算机关机后就会消失。要长久保存数据,就要使用硬盘、光盘、U 盘等设备。为了便于数据的管理和检索,引入了==“文件”==的概念。

一篇文章、一段视频、一个可执行程序,都可以被保存为一个文件,并赋予一个文件名。操作系统以文件为单位管理磁盘中的数据。一般来说,==文件可分为文本文件、视频文件、音频文件、图像文件、可执行文件等多种类别。==

image-20210315171013811

思考:文件操作包含哪些内容呢?

在日常操作中,我们对文件的主要操作:创建文件、打开文件、文件读写、文件备份等等

image-20210315171310117

文件操作的作用

文件操作的作用就是==把一些内容(数据)存储存放起来==,可以让程序下一次执行的时候直接使用,而不必重新制作一份,省时省力。

总结

Q1: 文件操作包括哪些内容?

  • 打开文件
  • 读写操作
  • 关闭文件

文件的基本操作

学习目标

  • 掌握绝对路径和相对路径的写法
  • 掌握文件的读取相关操作
  • 掌握文件的写入相关操作

open函数打开文件

在Python,使用open()函数,可以打开一个已经存在的文件,或者创建一个新文件,语法如下:

1
2
f = open(name, mode)
注:返回的结果是一个file文件对象(后续会学习,只需要记住,后续方法都是f.方法())

name:是要打开的目标文件名的字符串(可以包含文件所在的具体路径)。

mode:设置打开文件的模式(访问模式):只读r、写入w、追加a等。

r模式:代表以只读模式打开一个已存在的文件,后续我们对这个文件只能进行读取操作。如果文件不存在,则直接报错。另外,r模式在打开文件时,会将光标放在文件的第一行(开始位置)。

w模式:代表以只写模式打开一个文件,文件不存在,则自动创建该文件。w模式主要是针对文件写入而定义的模式。但是,要特别注意,w模式在写入时,光标也是置于第一行同时还会清空原有文件内容。

a模式:代表以追加模式打开一个文件,文件不存在,则自动创建该文件。a模式主要也是针对文件写入而定义模式。但是和w模式有所不同,a模式不会清空文件的原有内容,而是在文件的尾部追加内容。

文件路径:① 绝对路径 ② 相对路径

① 绝对路径:绝对路径表示绝对概念,一般都是从盘符开始,然后一级一级向下查找(不能越级),直到找到我们要访问的文件即可。

比如访问C盘路径下的Python文件夹下面的python.txt文件,其完整路径:

1
2
3
4
Windows
C:\Python\python.txt
Linux
/usr/local/nginx/conf/nginx.conf

绝对路径一般路径固定了,文件就不能进行移动,另外在迁移过程中会比较麻烦。

② ==相对路径==:相对路径表示相对概念,不需要从盘符开始,首先需要找到一个参考点(就是Python文件本身)

同级关系:我们要访问的文件与Python代码处于同一个目录,平行关系,同级关系的访问可以使用./文件名称或者直接写文件名称即可

上级关系:如果我们要访问的文件在当前Python代码的上一级目录,则我们可以通过../来访问上一级路径(如果是多级,也可以通过../../../去一层一层向上访问

下级关系:如果我们要访问的文件在与Python代码同级的某个文件夹中,则我们可以通过文件夹名称/来访问某个目录下的文件

入门级案例

1
2
3
4
5
6
# 1、打开文件
f = open('python.txt', 'w')
# 2、写入内容
f.write('人生苦短,我学Python!')
# 3、关闭文件
f.close()

强调一下:中文乱码问题,默认情况下,计算机常用编码ASCII、GBK、UTF-8

解决写入中文乱码问题

1
2
3
4
5
6
# 1、打开文件
f = open('python.txt', 'w', encoding='utf-8')
# 2、写入内容
f.write('人生苦短,我学Python!')
# 3、关闭文件
f.close()

文件的读取操作

read(size)方法:主要用于文本类型或者二进制文件(图片、音频、视频…)数据的读取

size表示要从文件中读取的数据的长度(单位是字符/字节),如果没有传入size,那么就表示读取文件中所有的数据。

1
2
f.read()  # 读取文件的所有内容
f.read(1024) # 读取1024个字符长度文件内容,字母或数字
1
2
3
4
5
6
7
# 1、打开文件
f = open('python.txt', 'r', encoding='utf-8')
# 2、使用read()方法读取文件所有内容
contents = f.read()
print(contents)
# 3、关闭文件
f.close()

readlines()方法:主要用于文本类型数据的读取

readlines可以按照行的方式把整个文件中的内容进行一次性读取,并且返回的是一个列表,其中每一行的数据为一个元素。

1
2
3
4
5
6
7
8
# 1、打开文件
f = open('python.txt', 'r', encoding='utf-8')
# 2、读取文件
lines = f.readlines()
for line in lines:
print(line, end='')
# 3、关闭文件
f.close()

readline()方法:一次读取一行内容,每运行一次readline()函数,其就会将文件的指针向下移动一行

1
2
3
4
5
6
7
8
9
10
11
12
13
f = open('python.txt’)

while True:
# 读取一行内容
content = file.readline()
# 判断是否读取到内容
if not content:
break
# 如果读取到内容,则输出
print(content)

# 关闭文件
f.close()

聊聊文件操作的mode模式

模式 描述
r 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。
rb 以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。
r+ 打开一个文件用于读写。文件指针将会放在文件的开头。
rb+ 以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。
w 打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb 以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
w+ 打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
wb+ 以二进制格式打开一个文件用于读写。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。
a 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
ab 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
a+ 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。
ab+ 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。

虽然mode文件操作模式很多,但是我们只需要记住3个字符即可。r、w、a

r+、w+、a+,代加号,功能全,既能读,又能写(区别在于指针到底指向不同)

rb、wb、ab,代b的字符,代表以二进制的形式对其进行操作,适合读取文本或二进制格式文件,如图片、音频、视频等格式

rb+、wb+、ab+,代加号,功能全,既能读,又能写(区别在于指针到底指向不同)

总结

Q1: 文件操作相关的函数有哪些?

  • 打开文件: open()
  • 读取数据: read(), readline(), readlines()
  • 关闭文件: close()

os模块

学习目标

  • 掌握os模块常用函数

os模块使用步骤

在Python中文件和文件夹的操作要借助os模块里面的相关功能,具体步骤如下:

第一步:导入os模块

1
import os

第二步:调用os模块中的相关方法

1
os.函数名()

文件操作相关方法

编号 函数 功能
1 os.rename(旧文件名称,新文件名称) 对文件进行重命名操作
2 os.remove(要删除文件名称) 对文件进行删除操作

案例:把Python项目目录下的python.txt文件,更名为linux.txt,休眠20s,刷新后,查看效果,然后对这个文件进行删除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 第一步:导入os模块
import os
# 第三步:引入time模块
import time


# 第二步:使用os.rename方法对python.txt进行重命名
os.rename('python.txt', 'linux.txt')

# 第四步:休眠20s
time.sleep(20)

# 第五步:删除文件(linux.txt)
os.remove('linux.txt')

文件夹操作相关操作

前提:

1
import os

相关方法:

编号 函数 功能
1 os.mkdir(新文件夹名称) 创建一个指定名称的文件夹
2 os.getcwd() current work directory,获取当前目录名称
3 os.chdir(切换后目录名称) change directory,切换目录
4 os.listdir(目标目录) 获取指定目录下的文件信息,返回列表
5 os.rmdir(目标目录) 用于删除一个指定名称的”空”文件夹

案例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 导入os模块
import os


# 1、使用mkdir方法创建一个images文件夹
if not os.path.exists('images'):
os.mkdir('images')

if not os.path.exists('images/avatar')
os.mkdir('images/avatar')

# 2、getcwd = get current work directory
print(os.getcwd())

# 3、os.chdir,ch = change dir = directory切换目录
os.chdir('images/avatar')
print(os.getcwd())

# 切换到上一级目录 => images
os.chdir('../../')
print(os.getcwd())

# 4、使用os.listdir打印当前所在目录下的所有文件,返回列表
print(os.listdir())

# 5、删除空目录
os.rmdir('images/avatar')

案例2:准备一个static文件夹以及file1.txt、file2.txt、file3.txt三个文件

① 在程序中,将当前目录切换到static文件夹

② 创建一个新images文件夹以及test文件夹

③ 获取目录下的所有文件

④ 移除test文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 导入os模块
import os

# ① 在程序中,将当前目录切换到static文件夹
os.chdir('File')
# print(os.getcwd())

# ② 创建一个新images文件夹以及test文件夹
if not os.path.exists('images'):
os.mkdir('images')

if not os.path.exists('test'):
os.mkdir('test')

# ③ 获取目录下的所有文件
# print(os.listdir())
for file in os.listdir():
print(file)

# ④ 移除test文件夹
os.rmdir('test')

文件夹删除补充(递归删除、慎重!)

1
2
3
4
5
# 导入shutil模块
import shutil

# 递归删除非空目录
shutil.rmtree('要删除文件夹路径')

递归删除文件夹的原理:理论上,其在删除过程中,如果文件夹非空,则自动切换到文件夹的内部,然后把其内部的文件,一个一个删除,当所有文件删除完毕后,返回到上一级目录,删除文件夹本身。

with-open语句

它主要是针对于 文件操作的, 即: 你再也不用手动 close()释放资源了, 该语句会在 语句体执行完毕后, 自动释放资源.

格式:
​ with open(‘路径’, ‘模式’, ‘码表’) as 别名, open(‘路径’, ‘模式’, ‘码表’) as 别名:
​ 语句体

特点:
​ 语句体执行结束后, with后边定义的变量, 会自动被释放.

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 打开 数据源 和 目的地文件.
with open('./data/a.txt', 'rb') as src_f, open('./data/b.txt', 'wb') as dest_f:
# 2. 具体的 拷贝动作.
# 2.1 循环拷贝.
while True:
# 2.2 一次读取8192个字节.
data = src_f.read(8192)
# 2.3 读完了, 就不读了.
if len(data) <= 0:
# if data == '':
break
# 2.4 走到这里, 说明读到了, 把读取到的数据写出到目的地文件.
dest_f.write(data)

总结

Q1: OS模块的常用函数

  • rename()
  • remove()
  • mkdir()
  • getcwd()
  • chdir()
  • listdir()
  • rmdir()
  • shutil.rmtree()

文件案例

案例需求

反转文件内容.按行读取文件内容, 对每行的内容进行反转后, 写到另1个文件中.
例如:数据源文件: a.txt

1
2
3
4
5
6
7
8
9
数据源文件: a.txt
好好学习,
天天向上.
abc123!@#

目的地文件: b.txt
,习学好好
.上向天天
#@!321cba

实现思路

① 读取两个文件a.txt,并且将读取到的文件存储到一个列表中

② 遍历文件行中所有的内容,去除行末换行符,使用切片方式翻转内容

③ 将翻转后的内容加载到一个新的列表中

④将新列表的中的内容以文件的方式写到本地磁盘b.txt

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 1. 定义源文件路径和目标文件路径
source_file = "a.txt"
destination_file = "b.txt"

# 2. 打开源文件以读取模式 ('r')
with open(source_file, "r", encoding="utf-8") as src_file:
# 3. 读取源文件的所有行
lines = src_file.readlines()

# 4. 初始化一个列表用于存储反转后的行
reversed_lines = []

# 5. 遍历每行内容
for line in lines:
# 6. 去除行末的换行符
stripped_line = line.rstrip('\n')
# 7. 反转行内容
reversed_line = stripped_line[::-1]
# 8. 将反转后的行添加到列表中,并添加换行符
reversed_lines.append(reversed_line + '\n')

# 9. 打开目标文件以写入模式 ('w')
with open(destination_file, "w", encoding="utf-8") as dest_file:
# 10. 将反转后的行写入目标文件
dest_file.writelines(reversed_lines)

# 11. 打印操作完成的消息
print(f"文件内容已反转并保存到 {destination_file}")

巩固练习

拷贝文件并改名.例如: 把 a.py文件 拷贝到 a[备份].txt 文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 定义源文件路径
source_file = "a.py"

# 2. 通过字符串切片和拼接方式定义目标文件路径
base_name = source_file[:-3] # 去掉文件扩展名
destination_file = base_name + "[备份].txt"

# 3. 打开源文件以读取模式 ('r')
with open(source_file, "r", encoding="utf-8") as src_file:
# 4. 读取源文件的所有内容
content = src_file.read()

# 5. 打开目标文件以写入模式 ('w')
with open(destination_file, "w", encoding="utf-8") as dest_file:
# 6. 将源文件的内容写入目标文件
dest_file.write(content)

# 7. 打印操作完成的消息
print(f"文件 {source_file} 已拷贝并保存为 {destination_file}")

Python异常

学习目标

  • 掌握捕获异常的格式

什么是异常

当检测到一个错误时,解释器就无法继续执行了,反而出现了一些错误的提示,这就是所谓的”异常”。

异常演示

1
2
3
4
5
6
7
# 除数为0
# print(10/0)

# 文件读取异常
f = open('python.txt', 'r')
content = f.readlines()
print(content)

异常捕获

基本语法:

1
2
3
4
try:
可能发生错误的代码
except:
如果出现异常执行的代码

try…except主要用于捕获代码运行时异常,如果异常发生,则执行except中的代码

案例:

1
2
3
4
5
6
7
8
try:
f = open('python.txt', 'r')
content = f.readline()
print(content)
except:
f = open('python.txt', 'w', encoding='utf-8')
f.write('发生异常,执行except语句中的代码')
f.close()

捕获异常并输出错误信息

无论我们在except后面定义多少个异常类型,实际应用中,也可能会出现无法捕获的未知异常。这个时候,我们考虑使用Exception异常类型捕获可能遇到的所有未知异常:

1
2
3
4
try:
可能遇到的错误代码
except Exception as e:
print(e)

案例:打印一个未定义变量,使用Exception异常类进行捕获

1
2
3
4
try:
print(name)
except Exception as e:
print(e)

异常捕获中else语句

else语句:表示的是如果没有异常要执行的代码。

1
2
3
4
5
6
try:
print(1)
except Exception as e:
print(e)
else:
print('哈哈,真开森,没有遇到任何异常')

案例:

1
2
3
4
5
6
7
8
try:
f = open('python.txt', 'r')
except Exception as e:
print(e)
else:
content = f.readlines()
print(content)
f.close()

异常捕获中的finally语句

finally表示的是无论是否异常都要执行的代码,例如关闭文件

1
2
3
4
5
6
7
8
9
try:
f = open('python.txt', 'r')
except:
f = open('python.txt', 'w')
else:
content = f.readlines()
print(content)
finally:
f.close()

异常案例

案例需求

升级猜数字游戏,增加程序健壮性,用户在输入过程中可能不会输入数字或者不按照要求输入,程序要能捕获到用户的异常输入。在已有的猜数游戏中加入异常功能。

实现思路

①使用python异常捕获try…except,捕获用户异常输入

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import random


def guess_number_game():
# 1. 随机生成1个 1 ~ 100之间的数字, 让用户来猜.
guess_num = random.randint(1, 100)

while True:
try:
# 2. 键盘录入, 表示: 玩家出拳的编号.
input_num = int(input('请录入您要猜的整数 (1-100): '))

# 3. 判断用户是否猜对了, 并提示. 猜对, 猜大, 猜小.
if input_num == guess_num:
print('恭喜您, 猜对了, 请找夯老师领取奖品, 练习题一套!')
break
elif input_num > guess_num:
print('哎呀, 您猜大了!')
else:
print('哎呀, 您猜小了!')
except ValueError:
print('输入无效,请输入一个整数!')


# 调用猜数字游戏函数
guess_number_game()

总结

Q1: 什么是异常?

  • Python中, 把程序出现的所有非正常情况, 统称为异常.

Q2: 掌握捕获异常的格式.

try:

​ 可能出现问题的代码

Except exception as e:

​ 出现问题后的解决方案

else:

​ 如无异常, 则会执行这里的内容

finally:

​ 无论是否有问题, 都会执行这里的内容.

Python内置模块

学习目标

  • 理解Python模块的概念
  • 掌握Python导入模块的方式

什么是Python模块

Python 模块(Module),是一个==Python 文件==,以 .py 结尾,包含了 Python 对象定义和Python语句。模块能定义==函数,类和变量==,模块里也能包含可执行的代码。

1
2
3
4
5
import os		=>     os.py
import time => time.py
import random => random.py

random.randint(1, 10)

在Python中,模块通常可以分为两大类:==内置模块(目前使用的)== 和 ==自定义模块==

模块的导入方式

☆ import 模块名

☆ import 模块名 as 别名

☆ from 模块名 import *

☆ from 模块名 import 功能名

使用import导入模块

基本语法:

1
2
3
import 模块名称

import 模块名称1, 模块名称2, ...

使用模块中封装好的方法:

1
模块名称.方法()

案例:使用import导入math模块

1
2
3
4
import math

# 求数字9的平方根 = 3
print(math.sqrt(9))

案例:使用import导入math与random模块

1
2
3
4
import math, random

print(math.sqrt(9))
print(random.randint(-100, 100))

普及:我们在Python代码中,通过import方式导入的实际上都是文件的名称

使用as关键字为导入模块定义别名

在有些情况下,如导入的模块名称过长,建议使用as关键字对其重命名操作,以后在调用这个模块时,我们就可以使用别名进行操作。

1
2
3
4
import time as t

# 调用方式
t.sleep(10)

在Python中,如果给模块定义别名,命名规则建议使用大驼峰。

使用from…import导入模块

提问:已经有了import导入模块,为什么还需要使用from 模块名 import 功能名这样的导入方式?

答:import代表导入某个或多个模块中的所有功能,但是有些情况下,我们只希望使用这个模块下的某些方法,而不需要全部导入。这个时候就建议采用from 模块名 import 功能名

☆ from 模块名 import *

这个导入方式代表导入这个模块的所有功能(等价于import 模块名)

1
from math import *

☆ from 模块名 import 功能名(推荐)

1
from math import sqrt, floor

注意:以上两种方式都可以用于导入某个模块中的某些方法,但是在调用具体的方法时,我们只需要功能名()即可

1
功能名()

案例:

1
2
3
4
5
6
7
# from math import *
# 或
from math import sqrt, floor

# 调用方式
print(sqrt(9))
print(floor(10.88))

time模块中的time()方法

在Python中,time模块除了sleep方法以外,还有一个方法叫做time()方法

1
time.time()

主要功能:就是返回格林制时间到当前时间的秒数(时间戳)

案例:求循环代码的执行时间

1
2
3
4
5
6
7
8
9
10
11
12
import time

# 返回:格林制时间到当前时间的秒数
start = time.time()

# 编写一个循环
list1 = []
for i in range(1000000):
list1.append(i)

end = time.time()
print(f'以上代码共执行了{end - start}s')

总结

Q1: 模块指的是什么?

  • Python中, 把.py文件称之为模块.

Q2: 导入模块有哪些方式?

  • import …. as 别名
  • from … import … as 别名

Python中的自定义模块

学习目标

  • 理解什么是自定义模块

什么是自定义模块

在Python中,模块一共可以分为两大类:内置系统模块 和 自定义模块

模块的本质:在Python中,模块的本质就是一个Python的独立文件(后缀名.py),里面可以包含==全局变量、函数以及类==。

注:在Python中,每个Python文件都可以作为一个模块,模块的名字就是==文件的名字==。也就是说自定义模块名必须要符合标识符命名规则。

特别注意:我们在自定义模块时,模块名称不能为中文,不能以数字开头,另外我们自定义的模块名称不能和系统中自带的模块名称(如os、random)相冲突,否则系统模块的功能将无法使用。比如不能定义一个叫做os.py模块

定义一个自定义模块

案例:在Python项目中创建一个自定义文件,如my_module.py

1
2
def sum_num(num1, num2):
return num1 + num2

导入自定义模块

1
2
3
import 模块名称

from 模块名称 import 功能名

案例:

1
2
3
4
import my_module

# 调用my_module1模块中自定义的sum_num方法
print(my_module.sum_num(10, 20))

Python模块案例

案例需求

在python中,创建A模块定义求和函数, 在B模块中调用A模块中的函数.

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# module_a.py

def sum_numbers(numbers):
"""计算列表中所有数字的和"""
return sum(numbers)


# module_b.py
# 导入A模块
import module_a

# 定义一个数字列表
numbers = [1, 2, 3, 4, 5]

# 调用A模块中的求和函数
total_sum = module_a.sum_numbers(numbers)

# 输出结果
print(f"列表 {numbers} 的和是: {total_sum}")

巩固练习

在python中,创建A模块定义打印水仙花数函数, 在B模块中调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"""
需求: 练: A模块定义打印水仙花数函数, 在B模块中调用.
"""

# A模块
# 1. 定义打印水仙花数的函数
def print_narcissistic_numbers():
# 2. 遍历100到999之间的所有数字
for num in range(100, 1000):
# 3. 计算数字的每一位
hundreds = num // 100
tens = (num // 10) % 10
units = num % 10

# 4. 计算水仙花数
if num == hundreds ** 3 + tens ** 3 + units ** 3:
# 5. 打印水仙花数
print(num)

# B模块
# 1. 导入A模块中的打印水仙花数函数
from module_a import print_narcissistic_numbers

# 2. 调用打印水仙花数函数
print_narcissistic_numbers()

学生管理系统

案例需求

使用python语言实现学生管理系统,要求进入系统显示系统功能界面,功能如下:

1
2
3
4
5
6
添加学生信息
删除学生信息
修改学生信息
查询学生信息
遍历所有学生信息
退出系统

系统共6个功能,用户根据自己需求选取,实现该需求

实现思路

①构建系统功能菜单界面封装函数

②使用函数封装功能实现对学生信息添加、删除、修改、查询、遍历和退出

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# 1. 定义函数 print_info()  , 打印提示信息.
def print_info():
print('1. 添加学生信息')
print('2. 删除学生信息')
print('3. 修改学生信息')
print('4. 查询单个学生信息')
print('5. 查询所有的学生信息')
print('6. 退出系统')


# 3.1 定义容器, 用来存储学生信息, 格式如下:
# [{'id':'heima001', 'name':'刘亦菲', 'tel': '111'}, {'id':'heima002', 'name':'高圆圆', 'tel': '222'}]
info = []

# 3. 自定义函数 add_info(), 实现: 添加学生, 编号必须唯一
def add_info():
# 3.2 提示用户录入 学生的学号并接收.
new_id = input('请录入您要添加的学生的学号: ') # heima001, heima002, abc...

# 3.3 判断, 用户录入的学号是否存在, 如果存在, 就提示 学号存在, 然后添加结束. 如果不存在, 就提示录入 其它信息, 接收并存储.
for stu in info: # stu的格式, {'id':'heima001', 'name':'刘亦菲', 'tel': '111'}
if stu['id'] == new_id:
# 走这里, 说明学号存在
print('您录入的学号已存在, 请重新操作')
break
else:
# 走这里, 说明没有走break, 说明 录入的学号不存在, 就提示录入其它信息, 并添加.
new_name = input('请录入您要添加的学生的姓名: ')
new_tel = input('请录入您要添加的学生的手机号: ')
# 3.4 将用户录入的数据封装成字典.
stu_dict = {'id': new_id, 'name': new_name, 'tel': new_tel}
# 3.5 将上述的字典(一个学生的信息), 添加到 学生列表中(info)
# global info 说明我们在操作全局变量, 但是因为 info是一个列表, 它是可变类型, 所以形参的改变直接影响实参.
info.append(stu_dict)


# 4. 自定义函数 delete_info(), 实现: 删除学生, 根据编号删除
def delete_info():
# 4.1 提示用户录入要删除的学生学号, 并接收.
del_id = input('请录入您要删除的学生的id:')
# 4.2 判断该学号是否存在.
for stu in info:
# stu 就是已经存在的每一个学生的信息, 格式为: {'id':'heima001', 'name':'刘亦菲', 'tel': '111'}
if stu['id'] == del_id:
# 4.3 如果学号存在就删除该学生.
info.remove(stu) # 删除学生信息
print(f'学号为 {del_id} 的学生信息已删除成功!')
break
else:
# 4.4 如果学号不存在, 就提示: 该学号不存在, 请重新操作.
print('您录入的学号不存在, 请重新操作')


# 5. 自定义函数 update_info(), 实现: 修改学生信息, 根据编号修改, 只能修改: 姓名, 手机号.
def update_info():
# 5.1 提示用户录入要修改的学生学号, 并接收.
update_id = input('请录入您要修改的学生的id:')
# 5.2 判断该学号是否存在.
for stu in info:
# stu 就是已经存在的每一个学生的信息, 格式为: {'id':'heima001', 'name':'刘亦菲', 'tel': '111'}
if stu['id'] == update_id:
# 5.3 如果学号存在就修改 该学生信息.
new_name = input('请录入您要修改的学生的姓名: ')
new_tel = input('请录入您要修改的学生的手机号: ')
# 5.4 根据录入的信息, 修改元素值.
stu['name'] = new_name
stu['tel'] = new_tel

print(f'学号为 {update_id} 的学生信息已修改成功!')
break
else:
# 4.4 如果学号不存在, 就提示: 该学号不存在, 请重新操作.
print('您录入的学号不存在, 请重新操作')


# 6. 自定义函数 search_info(), 实现: 查询某个学生信息, 根据姓名查询
def search_info():
# 6.1 提示用户录入要查询的学生的姓名, 并接收.
search_name = input('请录入您要查询的学生的姓名:')

# 6.5 我们用bool类型的变量标记是否查找到学生, True: 有这个人, False: 没有这人
# 6.5.1 初值为 False, 假设没有这个学生.
flag = False

# 6.2 判断该姓名是否存在.
for stu in info:
# stu 就是已经存在的每一个学生的信息, 格式为: {'id':'heima001', 'name':'刘亦菲', 'tel': '111'}
if stu['name'] == search_name:
# 6.3 如果姓名存在就打印该学生.
print(stu)
# 6.5.2 查找到学生了, 就将flag的值改为 True
flag = True

# 6.5.3 判断是否查询到学生.
# if flag == False:
if not flag: # 逻辑运算符, and, or, not
# 6.4 如果姓名不存在, 就提示: 查无此人, 请重新操作.
print('查无此人, 请重新操作')


# 7. 自定义函数 search_all(), 实现: 查询所有学生的信息.
# 数据格式为: [{'id':'heima001', 'name':'刘亦菲', 'tel': '111'}, {'id':'heima002', 'name':'高圆圆', 'tel': '222'}]

def search_all():
# 7.1 判断是否有学生信息, 如果没有则提示用户先添加再查询.
if len(info) == 0:
print('没有学生信息, 请先添加, 后查询!')
else:
# 7.2 如果有, 则打印所有学生信息.
# 直接遍历所有.
for stu in info:
print(f'学生id:{stu["id"]}, 学生姓名:{stu["name"]}, 学生手机号:{stu["tel"]}')


# 2. 自定义while True循环逻辑, 实现: 用户录入什么数据, 就进行相应的操作
# 注意: 处理一下非法值.
# if __name__ == '__main__':
while True:
# 2.1 打印操作界面信息.
print_info()

# 2.2 提示用户录入他/她要进行操作的数字, 并接收.
num = input('请录入您要操作的数字: ')
if num == '1':
add_info()
elif num == '2':
delete_info()
elif num == '3':
update_info()
elif num == '4':
search_info()
elif num == '5':
search_all()
elif num == '6':
print('谢谢您的使用, 让我们下次再会!')
break # 记得结束循环
else:
print('没有这样的数字, 录入有误, 请重新录入')
print() # 目的, 更好的展示结果.

面向对象进阶

子类重写父类的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 小明掌握了老师傅和黑马的技术后,
# 自己潜心钻研出一套自己的独门配方的全新摊煎饼果子技术。
"""
重写解释:
概念:重写也叫覆盖,即子类出现和父类【重名】的属性或者方法 。称之为重写
调用层次:就近原则 子类用子类的,没有就去找父类的,依次按照继承顺序去找其他父类
"""


class Master:
def __init__(self):
self.kongfu = "[古法煎饼果子配方]"

def make_cake(self):
print(f"运用{self.kongfu}制作煎饼果子")


class School:
def __init__(self):
self.kongfu = "[黑马AI煎饼果子配方]"

def make_cake(self):
print(f"运用{self.kongfu}制作煎饼果子")


class Prentice(School, Master):
# 创新
def __init__(self):
self.kongfu = "[独创的煎饼果子配方]"

def make_cake(self):
print(f"运用{self.kongfu}制作煎饼果子")

xm=Prentice()
print(xm.make_cake())


子类访问父类功能

  • 方式1: 父类名.父类功能名(self)

    1741916481470

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    """
    很多顾客都希望能吃到徒弟做出的有自己独立品牌的煎饼果子,
    也有黑马配方技术的煎饼果子味道。

    语法格式:
    1:父类名.父类函数名(self) 精准访问,想找哪个父类,就调用哪个父类
    2: super().父类函数名() 只能访问最近的那个父类,有就用,没有就继续往后找,找不到就报错
    """


    class Master:
    def __init__(self):
    self.kongfu = "[古法煎饼果子配方]"
    def make_cake(self):
    print(f"Master:运用{self.kongfu}制作煎饼果子")
    class School:
    def __init__(self):
    self.kongfu = "[黑马AI煎饼果子配方]"
    def make_cake(self):
    print(f"School:运用{self.kongfu}制作煎饼果子")
    class Prentice(School, Master):
    # 创新
    def __init__(self):
    self.kongfu = "[独创的煎饼果子配方]"
    def make_cake(self):
    print(f"运用{self.kongfu}制作煎饼果子")

    # 需求:同时满足 独创 & 古法
    def make_master_cake(self):
    School.__init__(self) # 这里需要初始化父类的self。不然还会调用子类的self 打印独创
    Master.make_cake(self)
    def make_school_cake(self):
    Master.__init__(self) # 这里需要初始化父类的self。不然还会调用子类的self 打印独创
    School.make_cake(self)
    p = Prentice()
    print(p.kongfu) # 独创
    p.make_cake() # 独创
    p.make_master_cake() # 独创 古法?



  • 方式2: super().父类功能名()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    # 很多顾客都希望能吃到徒弟做出的有自己独立品牌的煎饼果子,
    # 也有黑马配方技术的煎饼果子味道。
    class Master:
    pass
    # def __init__(self):
    # self.kongfu = "[古法煎饼果子配方]"
    # def make_cake(self):
    # print(f"Master:运用{self.kongfu}制作煎饼果子")
    class School:
    pass
    # def __init__(self):
    # self.kongfu = "[黑马AI煎饼果子配方]"
    # def make_cake(self):
    # print(f"School:运用{self.kongfu}制作煎饼果子")
    class Prentice(Master,School):
    # 创新
    def __init__(self):
    self.kongfu = "[独创的煎饼果子配方]"
    def make_cake(self):
    print(f"运用{self.kongfu}制作煎饼果子")

    def make_old_cake(self):
    super().__init__()
    super().make_cake()

    p = Prentice()
    print(p.kongfu)
    p.make_cake()
    print("*"*24)
    p.make_old_cake()

封装入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 小明把技术传承给徒弟的同时,不想把自己的私房钱($5000000)
# 继承给徒弟,这时就要为钱这个属性设置私有权限。

"""
封装简介:
概述:属于面向对象三大特征之一,就是隐藏对象的属性和方法细节,仅对外提供公共访问方式
怎么封装&格式?
属性:__属性
方法: __方法名()
为什么要封装:
1:提供代码的安全性 私有化来保障
2:提高代码复用性 由函数来保障
弊端:代码量增加(公共访问接口),因为私有内容外界想访问,必须提供公共的接口。代码量增加了
"""


class Master:
def __init__(self):
self.kongfu = "[古法煎饼果子配方]"

def make_cake(self):
print(f"Master:运用{self.kongfu}制作煎饼果子")


class School:
def __init__(self):
self.kongfu = "[黑马AI煎饼果子配方]"

def make_cake(self):
print(f"School:运用{self.kongfu}制作煎饼果子")
class Prentice(School, Master):
# 创新
def __init__(self):
self.kongfu = "[独创的煎饼果子配方]"
self.__money = 20000 # 有钱

def __make_cake(self):
print(f"Prentice:运用{self.kongfu}制作煎饼果子")

# 我需要时不时给tusun展示一下我的实力
def get_money(self):
return self.__money

def set_money(self,money):
self.__money=money
class TuSun(Prentice):
pass
# ts = TuSun()
# print(ts.kongfu)
# ts.make_cake()
# # print(ts.money) # 这里已经将师傅的钱私有了。即tusun拿不到money这个属性了
# # AttributeError: 'TuSun' object has no attribute 'money'
# print(ts.get_money()) # 师傅对外提供的访问接口。只能通过这个接口来访问
# ts.set_money(10000)
# print(ts.get_money())
#
# #建议你私有化属性后,给属性提供公共访问接口 按照 getxxx setxxxx


#测试私有化方法
ts = TuSun()
print(ts.make_cake())

多态入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 定义动物类
class Animal:
def speak(self): # 叫抽象的、
print("叫一声")


class Dog(Animal):
def speak(self):
print("狗:汪汪叫")


class Cat(Animal):
def speak(self):
print("猫:喵喵叫")


def make_noise(an: Animal):
an.speak()


make_noise(Dog())
make_noise(Cat())

print(1)
print("str")

#
# #简单
# d = Dog()
# # d.speak()
# c = Cat()
# # c.speak()
# #演示多态
# # make_noise(d)
# # make_noise(c)
#
# #父类引用指向子类对象
# an:Animal=Dog() #
# an.speak()
#
# an:Animal=Cat() #
# an.speak()
#
# # #父类引用指向父类对象
# # an:Animal=Animal()
# # an.speak()
#

多态案例_构建对战平台

1741924352691

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 构建对象对战平台object_play
# 1 英雄一代战机(战斗力60)与敌军战机(战斗力70)对抗。
# 英雄1代战机失败!
# 2 卧薪尝胆,英雄二代战机(战斗力80)出场!,战胜敌军战机!
# 3 对象对战平台object_play, 代码不发生变化的情况下, 完成多次战斗
"""
概述:
专业版:同一个函数,接受不同的参数,有不同的效果
大白话:同一个事务在不同的时刻中表现出不同的状态
python中多态:前提条件
1:要有继承
2:要有方法重写
3:父类引用指向子类对象
"""


class HeroFighter:
def power(self):
return 60


class EnemyFighter:
def power(self):
return 70


class TwoHeroFighter(HeroFighter):
def power(self):
return 80


# 注意缩进: 构建对战平台, 公共的函数,接受不同的参数
def obejct_play(hero:HeroFighter, enemy):
if hero.power() >= enemy.power():
print("英雄机赢了")
else:
print("英雄机 惜败")


h1 = HeroFighter()
e1 = EnemyFighter()
h2=TwoHeroFighter()
obejct_play(h1,e1)
obejct_play(h2,e1)



抽象类案例_空调案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
"""
抽象类(接口) 一般充当父类,是一种标准(行业标准)
抽象类:包含有抽象方法的类称之为抽象类
抽象方法:方法体是空实现(pass)
"""


# 抽象类
class AC:
def cool_wind(self):
pass

def hot_wind(self):
pass

def wind_l_r(self):
pass

#实现父类(AC)中所有抽向方法
class Haier(AC):
def cool_wind(self):
print("吹冷风")

def hot_wind(self):
print("吹热风")

# def wind_l_r(self):
# print("摆头吹")


class Gree(AC):
def cool_wind(self):
print("吹冷风")

def hot_wind(self):
print("吹热风")

def wind_l_r(self):
print("摆头吹")


h = Haier()
h.cool_wind()
h.hot_wind()
# h.wind_l_r()

g=Gree()
g.cool_wind()
g.hot_wind()
g.wind_l_r()

对象属性和类型属性解释

  • 图解

    1741935509782

  • 代码演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    """
    属性介绍:
    概述:
    他是一个用来描述事物特征,他是名字
    分类:
    对象属性:属于每个对象,即:每个对象的属性值可能不同.
    类属性:属于类的,即:能被该类下所有的对象所共享
    对象属性:
    定义到 init魔法方法的属性。每个对象都有自己的内容
    只能通过对象名.的方式调用
    类属性:
    定义到类中,函数外的属性(变量),能被该类下所有的对象所共享
    既能通过 类名. 还能通过 对象名. 的方式来调用,推荐 类名.方式
    """


    class Student:
    # 类属性 :类中函数外
    teacher_name = "张老师"

    # 定义对象属性 即:定义到 init魔法方法的属性
    def __init__(self, name):
    self.name = name

    def __str__(self):
    return f"姓名{self.name}"


    s1 = Student("孔老师")
    print(s1)

    s2 = Student("王老师")
    print(s2)
    # 类属性 类名.属性
    print(Student.teacher_name)
    # 共享
    print(s1.teacher_name)
    print(s2.teacher_name)

    # 改类属性
    Student.teacher_name = "胡老师"
    print(s1.teacher_name)
    print(s2.teacher_name)
    print(Student.teacher_name)

类方法和静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"""
类方法:
属于类的方法,可以通过 类名.方式进行调用 还可以通过 对象名.方式
定义类方法的时候,必须使用装饰器@classmethod ,且第一个参数必须类对象
静态方法:
属于该类的所有对象所共享的方法,可以通过类名.方式 还可以通过 对象名.方式
定义静态方法的时候,必须使用装饰器 @staticmethod,且参数传不传都可以
区别:
1:类方法第一个参数必须是类对象,静态方法对参数无特殊要求
2:理解为:如果函数中要用到我们的类对象 ,就定义为类方法 ,否则定义为静态方法。
"""
#第一步:定义一个类
class Student:
#第二步:类属性
school="黑马"

@classmethod
def show1(cls):
print("aaa")
print(cls.school)
print("我是类方法")

@staticmethod
def show2():
print(Student.school)
print("我是静态方法")

s1 = Student()
# s1.show1()

# s1.show2("a")





学生管理系统_学生类代码编写

如下是写到 student.py 文件中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""
该文件用于记录学生类 ,学生属性信息有姓名、性别、年龄、联系方式、描述信息等;
"""
class Student:
"""
对象属性 :__init__方法中
"""
def __init__(self, name, gender, age, phone, desc):
self.name = name
self.gender = gender
self.age = age
self.phone = phone
self.desc = desc

# 可以使用魔法方法进行统一格式输出
def __str__(self):
return f"姓名:{self.name},性别{self.gender},年龄{self.age},手机号{self.phone},描述信息{self.desc}"

# if __name__ == '__main__':
s1=Student("张三","男",20,"12343242342","这是一个好人")

# print(f"我是测试案例{s1}")

# main 回车键
#
if __name__ == '__main__':
s1 = Student("张三", "男", 20, "12343242342", "这是一个好人")
#
#



学生管理系统_框架搭建

如下是写到 studentcms.py 文件中的内容.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
"""
该类的作用就是完成学生管理操作 :即增删改查
"""
class StudentCMS:
#实现功能(定义方法)
@staticmethod
def show_view():
print("*"*24)
print("学生管理系统V2.0面向对象版本")
print("1.添加学生信息")
print("2.修改学生信息")
print("3.删除学生信息")
print("4.查询某个学生信息")
print("5.显示所有学生信息")
print("6.保存学生信息")
print("0.退出系统")
print("*" * 24)

def add_student(self):
print("add_student")

def update_student(self):
print("update_student")

def del_student(self):
print("del_student")

def search_one_student(self):
print("search_one_student")

def search_all_student(self):
print("search_all_student")

def save_student(self):
print("save_student")

def exit_student(self):
print("exit_student")

def star(self):
while True:
StudentCMS.show_view()
input_num =input("请您输入要操作编号")
if input_num =="1":
self.add_student()
elif input_num =="2":
self.update_student()
elif input_num=="3":
self.del_student()
elif input_num =="4":
self.search_one_student()
elif input_num=="5":
self.search_all_student()
elif input_num=="6":
self.save_student()
elif input_num=="0":
self.exit_student()
else:
print("您输入的有误!!!")

if __name__ == '__main__':
cms = StudentCMS()
cms.star()




学生管理系统_入口文件

如下的代码是写到 main.py 文件中的.

1
2
3
4
5
6
7
8
9
10
11
"""
该文件 作为整个程序的入口文件
"""

from studentcms import StudentCMS

if __name__ == '__main__':
#我只在这个main.py文件中创建对象
stu_cms=StudentCMS()
stu_cms.star()

学生管理系统_功能实现

  • 添加学生

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 4. 定义函数, 实现添加学生信息功能.
    def add_student(self):
    # 4.1 提示用户输入学生信息, 并接收.
    name = input('请输入学生姓名:')
    gender = input('请输入学生性别:')
    age = int(input('请输入学生年龄:'))
    phone = input('请输入学生电话:')
    desc = input('请输入学生描述信息:')
    # 4.2 把上述的信息封装成学生对象.
    stu = Student(name, gender, age, phone, desc)
    # 4.3 把学生对象添加到列表中.
    self.stu_list.append(stu)
    # 4.4 提示.
    print(f'添加 {name} 学生信息成功!\n')
  • 查看所有学生信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 8. 定义函数, 实现查询所有学生信息功能.
    def search_all_student(self):
    # 8.1 判断列表长度是否为0, 如果为0, 提示: 暂无学生信息, 请添加后查询.
    if len(self.stu_list) == 0:
    print('暂无学生信息, 请添加后查询! \n')
    else:
    # 8.2 如果长度不为0, 遍历列表, 打印出所有的学生信息.
    for stu in self.stu_list:
    print(stu)
    print() # 为了格式好看, 加个换行.
  • 删除学生信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 5. 定义函数, 实现删除学生信息功能.
    def del_student(self):
    # 5.1 提示用户输入要删除的学生的姓名, 并接收.
    del_name = input('请输入要删除的学生姓名:')
    # 5.2 遍历列表, 找到要删除的学生, 并删除.
    for stu in self.stu_list:
    # 5.3 如果当前学生的姓名 和 要删除的学生相同, 就删除该学生信息
    if stu.name == del_name:
    self.stu_list.remove(stu)
    print(f'学员 {del_name} 信息删除成功!\n')
    break
    else:
    # 走到这里, 说明没有走break, 即: 没有找到这个学生.
    print('查无此人, 请检查后重新删除!\n')
  • 修改学生信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 6. 定义函数, 实现修改学生信息功能.
    def update_student(self):
    # 6.1 提示用户输入要修改的学生的姓名, 并接收.
    upd_name = input('请输入要修改的学生姓名:')
    # 6.2 遍历列表, 找到要修改的学生, 并修改.
    for stu in self.stu_list:
    # 6.3 如果当前学生的姓名 和 要修改的学生相同, 就修改该学生信息
    if stu.name == upd_name:
    # 6.4 提示用户录入该学员新的信息.
    stu.gender = input('请录入修改后的性别: ')
    stu.age = int(input('请录入修改后的年龄: '))
    stu.phone = input('请录入修改后的电话: ')
    stu.desc = input('请录入修改后的描述信息: ')

    print(f'学员 {upd_name} 信息修改成功!\n')
    break
    else:
    # 走到这里, 说明没有走break, 即: 没有找到这个学生.
    print('查无此人, 请检查后重新操作!\n')
  • 查询单个学生信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 7. 定义函数, 实现查询单个学生信息功能.
    def search_one_student(self):
    # 7.1 提示用户输入要查找的学生的姓名, 并接收.
    search_name = input('请输入要查找的学生姓名:')
    # 7.2 遍历列表, 找到要查找的学生, 并打印信息.
    for stu in self.stu_list:
    # 7.3 如果当前学生的姓名 和 要查找的学生相同, 就打印该学生信息
    if stu.name == search_name:
    print(stu, end='\n\n')
    break
    else:
    # 走到这里, 说明没有走break, 即: 没有找到这个学生.
    print('查无此人, 请检查后重新操作!\n')

扩展_dict属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""
__dict__属性介绍
他是python内置的。可以把对象转换为字典
"""
from student import Student
#
# s1 =Student("周云杰","男",60,"421313214","海尔老总")
# s2 =Student("周云杰2","男",30,"421313214","海尔小总")
# s3 =Student("周云杰3","男",35,"421313214","海尔总总")
# stu_list=[s1,s2,s3]
#
# #列表推导式
# list_change_dict=[stu.__dict__ for stu in stu_list]
# print(list_change_dict)

#需求:字典转换为对象
my_dict ={'name': 'zhouyunjie', 'gender': '男', 'age': 30, 'phone': '43241421', 'desc': '海尔老总'}
s6=Student(**my_dict)
print(s6)
print(type(my_dict))
print(type(s6))





#
#
# print(s1)
# #想 {'name':'周云杰','gender':'男' ....}
# mydicts1=s1.__dict__
# print(mydicts1)

学生管理学系统_保存学生信息

1
2
3
4
5
6
7
8
# 9. 定义函数, 实现保存学生信息功能.
def save_student(self):
# 9.1 关联 学生信息文件.
with open('./stu_data.txt', 'w', encoding='utf-8') as dest_f:
# 9.2 把 [学生对象, 学生对象...] -> [字典, 字典...]
stu_dict = [stu.__dict__ for stu in self.stu_list]
# 9.3 把字典列表, 持久化到文件中.
dest_f.write(str(stu_dict)) # 记得转成字符串再写入.

学生管理系统_加载学生信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 10. 定义函数, 实现加载学生信息.
def load_student(self):
# 10.1 加入异常处理, 有可能文件不存在.
try:
# 10.2 关联学生信息文件.
with open('./stu_data.txt', 'r', encoding='utf-8') as src_f:
# 10.3 一次性读取所有数据.
stu_data = src_f.read() # '[字典, 字典...]'
# 10.4 把上述的字符串, 转为列表.
stu_list = eval(stu_data) # ''
# 10.5 判断如果列表为空, 就赋予空列表.
if len(stu_list) == 0:
stu_list = []
# 10.6 把stu_list(列表套字典) 转成 [学生对象, 学生对象...], 并赋值给 self.stu_list
self.stu_list = [Student(**stu_dict) for stu_dict in stu_list]
except:
# 10.7 走这里, 说明目的地文件不存在, 创建即可.
with open('./stu_data.txt', 'w', encoding='utf-8') as src_f:
pass

学生管理系统_最终代码

  • student.py 文件中的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    """
    该文件用于记录学生类 ,学生属性信息有姓名、性别、年龄、联系方式、描述信息等;
    """
    class Student:
    """
    对象属性 :__init__方法中
    """
    def __init__(self, name, gender, age, phone, desc):
    self.name = name
    self.gender = gender
    self.age = age
    self.phone = phone
    self.desc = desc

    # 可以使用魔法方法进行统一格式输出
    def __str__(self):
    return f"姓名:{self.name},性别{self.gender},年龄{self.age},手机号{self.phone},描述信息{self.desc}"

    # if __name__ == '__main__':
    s1=Student("张三","男",20,"12343242342","这是一个好人")

    # print(f"我是测试案例{s1}")

    # main 回车键
    #
    if __name__ == '__main__':
    s1 = Student("张三", "男", 20, "12343242342", "这是一个好人")
    #
    #



  • studentcms.py 文件中的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    """
    该类的作用就是完成学生管理操作 :即增删改查
    """

    # import student as st
    from student import Student
    import os
    class StudentCMS:
    def __init__(self):
    self.stu_list = [] # 主要使用用于存储学生信息 [stu,stu,stu,stu] 面向对象

    # 实现功能(定义方法)
    @staticmethod
    def show_view():
    print("*" * 24)
    print("学生管理系统V2.0面向对象版本")
    print("1.添加学生信息")
    print("2.修改学生信息")
    print("3.删除学生信息")
    print("4.查询某个学生信息")
    print("5.显示所有学生信息")
    print("6.保存学生信息")
    print("7.加载学生信息")
    print("0.退出系统")
    print("*" * 24)
    def add_student(self):
    name = input("请输入学生姓名")
    gender = input("请输入学生性别")
    age = int(input("请输入学生年龄"))
    phone = input("请输入学生电话")
    desc = input("请输入学生描述")
    stu = Student(name, gender, age, phone, desc)
    self.stu_list.append(stu)
    print(f"添加{name}学生信息成功")

    def update_student(self):
    upd_name = input("请输入要修改学生姓名")
    for stu in self.stu_list:
    if stu.name == upd_name:
    stu.gender = input("请输入您要修改的性别")
    stu.age = input("请输入您要修改的年龄")
    stu.phone = input("请输入您要修改的手机号")
    stu.desc = input("请输入您要修改的描述信息")
    print(f"学生{upd_name}信息修改成功")
    break
    else:
    print("查无此人")
    def del_student(self):
    del_name = input("请输入您要删除学生姓名")
    for stu in self.stu_list:
    if stu.name == del_name:
    self.stu_list.remove(stu)
    print(f"学员{del_name}信息删除成功")
    break
    else:
    print("查无此人")

    def search_one_student(self):
    search_name = input("请输入您要查找学生姓名:")
    for stu in self.stu_list:
    if stu.name == search_name:
    print(stu)
    break
    else:
    print("查无此人")
    # print("search_one_student")

    def search_all_student(self):
    if len(self.stu_list) == 0:
    print("暂无学生信息")
    else:
    for stu in self.stu_list:
    print(stu)

    def save_student(self):
    with open("./stu_data.txt","w",encoding="utf-8") as dets_f:
    stu_dict= [stu.__dict__ for stu in self.stu_list]
    # for stu in self.stu_list:
    # dets_f.write(str(stu))
    dets_f.write(str(stu_dict))

    def exit_student(self):
    os._exit(0)

    # 加载.txt文本文件中的学生信息
    # 读文件
    # 遍历将文件内容加载列表中
    def load_student(self):
    try: #万一没有文件呢!!!
    with open("./stu_data.txt","r",encoding="utf-8") as src_f:
    stu_data=src_f.read() #[字典,字典,字典]
    #把里面的字符串,根据字符串特点,推断合适格式
    stu_list=eval(stu_data)
    if len(stu_list)==0:
    stu_list=[]
    #把列表套字典转换为[学生对象,学生对象,学生对象...] 赋值 self.stu_list
    self.stu_list=[Student(**stu_dict) for stu_dict in stu_list]
    except:
    with open("./stu_data.txt","w",encoding="utf-8") as src_f:
    pass

    def star(self):
    while True:
    StudentCMS.show_view()
    input_num = input("请您输入要操作编号")
    if input_num == "1":
    self.add_student()
    elif input_num == "2":
    self.update_student()
    elif input_num == "3":
    self.del_student()
    elif input_num == "4":
    self.search_one_student()
    elif input_num == "5":
    self.search_all_student()
    elif input_num == "6":
    self.save_student()
    elif input_num == "7":
    self.load_student()
    elif input_num == "0":
    self.exit_student()
    else:
    print("您输入的有误!!!")


    if __name__ == '__main__':
    cms = StudentCMS()
    cms.star()

  • main.py 文件中的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    """
    该文件 作为整个程序的入口文件
    """

    from studentcms import StudentCMS

    if __name__ == '__main__':
    #我只在这个main.py文件中创建对象
    stu_cms=StudentCMS()
    stu_cms.star()

神经网络基础

神经网络

深度学习神经网络就是大脑仿生,数据从输入到输出经过一层一层的神经元产生预测值的过程就是==前向传播(也叫正向传播)==。

前向传播涉及到人工神经元是如何工作的(也就是神经元的初始化、激活函数),神经网络如何搭建,权重参数计算、数据形如何状变化。千里之行始于足下,我们一起进入深度学习的知识海洋吧。

神经网络概念

什么是神经网络

人工神经网络(Artificial Neural Network, 简写为ANN)也简称为神经网络(NN),是一种模仿生物神经网络结构和功能的计算模型。它由多个互相连接的人工神经元(也称为节点)构成,可以用于处理和学习复杂的数据模式,尤其适合解决非线性问题。人工神经网络是机器学习中的一个重要模型,尤其在深度学习领域中得到了广泛应用。

人脑可以看做是一个生物神经网络,由众多的神经元连接而成。各个神经元传递复杂的电信号,树突接收到输入信号,然后对信号进行处理,通过轴突输出信号。下图是生物神经元示意图:

1755683629885

当电信号通过树突进入到细胞核时,会逐渐聚集电荷。达到一定的电位后,细胞就会被激活,通过轴突发出电信号。

如何构建神经网络

神经网络是由多个神经元组成,构建神经网络就是在构建神经元。以下是神经网络中神经元的构建说明:

1755683648103

这个流程就像,来源不同树突(树突都会有不同的权重)的信息, 进行的加权计算, 输入到细胞中做加和,再通过激活函数输出细胞值。

同一层的多个神经元可以看作是通过并行计算来处理相同的输入数据,学习输入数据的不同特征。每个神经元可能会关注输入数据中的不同部分,从而捕捉到数据的不同属性。

接下来,我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图所示:

1755683664741

神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本部分是:

  1. 输入层(Input Layer): 即输入x的那一层(如图像、文本、声音等)。每个输入特征对应一个神经元。输入层将数据传递给下一层的神经元。
  2. 输出层(Output Layer): 即输出y的那一层。输出层的神经元根据网络的任务(回归、分类等)生成最终的预测结果。
  3. 隐藏层(Hidden Layers): 输入层和输出层之间都是隐藏层,神经网络的“深度”通常由隐藏层的数量决定。隐藏层的神经元通过加权和激活函数处理输入,并将结果传递到下一层。

==特点是:==

  • 同一层的神经元之间没有连接
  • 第N层的每个神经元和第N-1层的所有神经元相连(这就是Fully Connected的含义),这就是全连接神经网络(FCNN)
  • 全连接神经网络接收的样本数据是二维的,数据在每一层之间需要以二维的形式传递
  • 第N-1层神经元的输出就是第N层神经元的输入
  • 每个连接都有一个权重值(w系数和b系数)

神经网络内部状态值和激活值

1755683682418

每一个神经元工作时,前向传播会产生两个值,内部状态值(加权求和值)激活值反向传播时会产生激活值梯度内部状态值梯度

  • 内部状态值

    • 神经元或隐藏单元的内部存储值,它反映了当前神经元接收到的输入、历史信息以及网络内部的权重计算结果。
    • 每个输入$$x_i$$都有一个与之相乘的权重$$w_i​$$,表示每个输入信号的重要性。
    • z=w⋅x+b
      • w:权重矩阵
      • x:输入值
      • b:偏置
  • 激活值

    • 通过激活函数(如 ReLU、Sigmoid、Tanh)对内部状态值进行非线性变换后得到的结果。激活值决定了当前神经元的输出。
    • a=f(z)
      • f:激活函数
      • z:内部状态值

通过控制每个神经元的内部状态值、激活值的大小;每一层的内部状态值的方差、每一层的激活值的方差可让整个神经网络工作的更好。

所以下面两个小结,我们将要学习神经元的激活函数,神经元的权重初始化。

激活函数

网络非线性因素理解

  • 没有引入非线性因素的网络等价于使用一个线性模型来拟合

  • 通过给网络输出增加激活函数, 实现引入非线性因素, 使得网络模型可以逼近任意函数, 提升网络对复杂问题的拟合能力

激活函数用于对每层的输出数据进行变换, 进而为整个网络注入了非线性因素。此时, 神经网络就可以拟合各种曲线。如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型,如下公式所示:

1755683706029

另外通过图像可视化的形式理解:

神经网络可视化

1755683723098

我们发现增加激活函数之后, 对于线性不可分的场景,神经网络的拟合能力更强。

常见激活函数

激活函数主要用来向神经网络中加入非线性因素,以解决线性模型表达能力不足的问题,它对神经网络有着极其重要的作用。我们的网络参数在更新时,使用的反向传播算法(BP),这就要求我们的激活函数必须可微。

Sigmoid 激活函数

激活函数公式:

1755683738156

激活函数求导公式:

1755683750621

sigmoid 激活函数的函数图像如下:

1755683757387

  • 从sigmoid函数图像可以得到,sigmoid 函数可以将任意的输入映射到 (0, 1) 之间,当输入的值大致在**<-6或者>6**时,意味着输入任何值得到的激活值都是差不多的,这样会丢失部分的信息。比如:输入100和输入10000经过 sigmoid的激活值几乎都是等于1的,但是输入的数据之间相差100倍的信息就丢失了。

  • 对于sigmoid函数而言,输入值在**[-6, 6]之间输出值才会有明显差异**,输入值在**[-3, 3]之间才会有比较好的效果**

  • 通过上述导数图像,我们发现导数数值范围是 (0, 0.25),当输入的值**<-6或者>6时,sigmoid激活函数图像的导数接近为 0**,此时网络参数将更新极其缓慢,或者无法更新。

  • 一般来说,sigmoid网络在5层之内就会产生梯度消失现象。而且,该激活函数的激活值并不是以0为中心的,激活值总是偏向正数,导致梯度更新时,只会对某些特征产生相同方向的影响,所以在实践中这种激活函数使用的很少。sigmoid函数一般只用于二分类的输出层

在 PyTorch中使用sigmoid函数的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import torch
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

"""
绘制激活函数图像时出现以下提示,需要将anaconda3/Lib/site-packages/torch/lib目录下的libiomp5md.dll文件删除
OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
"""

# 创建画布和坐标轴
_, axes = plt.subplots(1, 2)

# 函数图像
x = torch.linspace(-20, 20, 1000)
# 输入值x通过sigmoid函数转换成激活值y
y = torch.sigmoid(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Sigmoid 函数图像')

# 导数图像
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.sigmoid(x).sum().backward()

# x.detach():输入值x的ndarray数组
# x.grad:计算梯度,求导
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Sigmoid 导数图像')

plt.show()
Tanh 激活函数

Tanh叫做双曲正切函数,其公式如下:

1755683773337

激活函数求导公式:

1755683779699

Tanh的函数图像、导数图像如下:

1755683786672

  • 由上面的函数图像可以看到,Tanh函数将输入映射到(-1, 1)之间,图像以0为中心,激活值在0点对称,当输入的值大概**<-3或者>3** 时将被映射为-1或者1。其导数值范围 (0, 1),当输入的值大概**<-3或者>3**时,其导数近似0。

  • 与Sigmoid相比,它是以0为中心的,使得其收敛速度要比Sigmoid快,减少迭代次数。然而,从图中可以看出,Tanh两侧的导数也为0,同样会造成梯度消失。

  • 若使用时可在隐藏层使用tanh函数,在输出层使用sigmoid函数

在 PyTorch 中使用tanh函数的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

_, axes = plt.subplots(1, 2)

# 函数图像
x = torch.linspace(-20, 20, 1000)
y = torch.tanh(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Tanh 函数图像')

# 导数图像
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.tanh(x).sum().backward()

axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Tanh 导数图像')

plt.show()
ReLU 激活函数

ReLU 激活函数公式如下:

1755683806041

激活函数求导公式:

1755683810166

ReLU 的函数图像、导数图像如下:

1755683816139

  • ReLU 激活函数将小于0的值映射为0,而大于0的值则保持不变,它更加重视正信号,而忽略负信号,这种激活函数运算更为简单,能够提高模型的训练效率。
  • 当x<0时,ReLU导数为0,而当x>0时,则不存在饱和问题。所以,ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。然而,随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新。这种现象被称为“神经元死亡”。
  • ReLU是目前==最常用的激活函数==。与sigmoid相比,RELU的优势是:
    • 采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,计算量相对大;而采用Relu激活函数,整个过程的计算量节省很多
    • sigmoid函数反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练;而采用relu激活函数,当输入的值>0时,梯度为1,不会出现梯度消失的情况
    • Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生

在 PyTorch 中使用ReLU函数的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

_, axes = plt.subplots(1, 2)

# 函数图像
x = torch.linspace(-20, 20, 1000)
y = torch.relu(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Tanh 函数图像')

# 导数图像
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.relu(x).sum().backward()

axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Tanh 导数图像')

plt.show()
SoftMax激活函数

softmax用于多分类过程中,它是二分类函数sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展现出来

计算方法如下图所示:

1755683834089

SoftMax就是将网络输出的logits通过softmax函数,映射成为(0,1)的值,而这些值的累和为1(满足概率的性质),那么我们将它理解成概率,选取概率最大(也就是值对应最大的)节点,作为我们的预测目标类别。

在 PyTorch 中使用SoftMax函数的示例代码如下:

1
2
3
4
5
6
7
import torch


scores = torch.tensor([0.2, 0.02, 0.15, 0.15, 1.3, 0.5, 0.06, 1.1, 0.05, 3.75])
# dim=0, 按行计算
probabilities = torch.softmax(scores, dim=0)
print(probabilities)

程序输出结果:

1
2
tensor([0.0212, 0.0177, 0.0202, 0.0202, 0.0638, 0.0287, 0.0185, 0.0522, 0.0183,
0.7392])

如何选择激活函数

除了上述的激活函数,还存在很多其他的激活函数,如下图所示:

1755683856039

对于隐藏层:

  1. 优先选择ReLU激活函数
  2. 如果ReLu效果不好,那么尝试其他激活,如Leaky ReLu等。
  3. 如果你使用了ReLU, 需要注意一下Dead ReLU问题,避免出现0梯度从而导致过多的神经元死亡。
  4. 少使用sigmoid激活函数,可以尝试使用tanh激活函数

对于输出层:

  1. 二分类问题选择sigmoid激活函数
  2. 多分类问题选择softmax激活函数
  3. 回归问题选择identity激活函数

参数初始化

我们在构建网络之后,网络中的参数是需要初始化的。我们需要初始化的参数主要有权重偏置偏置一般初始化为0即可,而对权重的初始化则会更加重要。

参数初始化的作用:

  • 防止梯度消失或爆炸:初始权重值过大或过小会导致梯度在反向传播中指数级增大或缩小。
  • 提高收敛速度:合理的初始化使得网络的激活值分布适中,有助于梯度高效更新。
  • 保持对称性破除:权重的初始化需要打破对称性,否则网络的学习能力会受到限制。

常见参数初始化方法

  • 随机初始化

    • 均匀分布初始化:权重参数初始化从区间均匀随机取值,默认区间为(0,1)。可以设置为在(-$$1\over\sqrt{d}$$,$$1\over\sqrt{d}​$$)均匀分布中生成当前神经元的权重,其中d为神经元的输入数量。

    • 正态分布初始化:随机初始化从均值为0,标准差是1的高斯分布中取样,使用一些很小的值对参数W进行初始化

    • 优点:能有效打破对称性

    • 缺点:随机选择范围不当可能导致梯度问题

    • 适用场景:浅层网络或低复杂度模型。隐藏层1-3层,总层数不超过5层。

  • 全0初始化:将神经网络中的所有权重参数初始化为0

    • 优点:实现简单
    • 缺点:无法打破对称性,所有神经元更新方向相同,无法有效训练
    • 适用场景:几乎不使用,仅用于偏置项的初始化
  • 全1初始化:将神经网络中的所有权重参数初始化为1

    • 优点:实现简单
    • 缺点
      • 无法打破对称性,所有神经元更新方向相同,无法有效训练
      • 会导致激活值在网络中呈指数增长,容易出现梯度爆炸
    • 适用场景
      • 测试或调试:比如验证神经网络是否能正常前向传播和反向传播
      • 特殊模型结构:某些稀疏网络或特定的自定义网络中可能需要手动设置部分参数为1
      • 偏置初始化:偶尔可以将偏置初始化为小的正值(如 0.1),但很少用1作为偏置的初始值
  • 固定值初始化:将神经网络中的所有权重参数初始化为某个固定值

    • 优点:实现简单
    • 缺点
      • 无法打破对称性,所有神经元更新方向相同,无法有效训练
      • 初始权重过大或过小可能导致梯度爆炸或梯度消失
    • 适用场景
      • 测试或调试
  • kaiming初始化,也叫做HE初始化:专为ReLU和其变体设计,考虑到ReLU激活函数的特性,对输入维度进行缩放

    • HE初始化分为正态分布的HE初始化、均匀分布的HE初始化
      • 正态分布的he初始化
        • w权重值从均值为0, 标准差为std中随机采样,std = sqrt(2 / fan_in)
        • std值越大,w权重值离均值0分布相对较广,计算得到的内部状态值有较大的正值或负值
      • 均匀分布的he初始化
        • 它从[-limit,limit] 中的均匀分布中抽取样本, limitsqrt(6 / fan_in)
      • fan_in 输入神经元的个数,当前层接受的来自上一层的神经元的数量。简单来说,就是当前层接收多少个输入
    • 优点:适合 ReLU,能保持梯度稳定
    • 缺点:对非 ReLU 激活函数效果一般
    • 适用场景:深度网络(10层及以上),使用 ReLU、Leaky ReLU 激活函数
  • xavier初始化,也叫做Glorot初始化:根据网络输入和输出的维度自动选择权重范围,使输入和输出的方差相同

    • xavier初始化分为正态分布的xavier初始化、均匀分布的xavier初始化

      • 正态化的Xavier初始化
        • w权重值从均值为0, 标准差为std中随机采样,std = sqrt(2 / (fan_in + fan_out))
        • std值越小,w权重值离均值0分布相对集中,计算得到的内部状态值有较小的正值或负值
      • 均匀分布的Xavier初始化
        • [-limit,limit] 中的均匀分布中抽取样本, limit 是 sqrt(6 / (fan_in + fan_out))
      • fan_in 是输入神经元个数,当前层接受的来自上一层的神经元的数量。简单来说,就是当前层接收多少个输入
      • fan_out 是输出神经元个数,当前层输出的神经元的数量,也就是当前层会传递给下一层的神经元的数量。简单来说,就是当前层会产生多少个输出。
    • 优点:适用于Sigmoid、Tanh 等激活函数,解决梯度消失问题

    • 缺点:对 ReLU 等激活函数表现欠佳

    • 适用场景:深度网络(10层及以上),使用 Sigmoid 或 Tanh 激活函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import torch.nn as nn


# 1. 均匀分布随机初始化
def test01():

linear = nn.Linear(5, 3)
# 从0-1均匀分布产生参数
nn.init.uniform_(linear.weight)
nn.init.uniform_(linear.bias)
print(linear.weight.data)


# 2. 固定初始化
def test02():

linear = nn.Linear(5, 3)
nn.init.constant_(linear.weight, 5)
print(linear.weight.data)


# 3. 全0初始化
def test03():

linear = nn.Linear(5, 3)
nn.init.zeros_(linear.weight)
print(linear.weight.data)


# 4. 全1初始化
def test04():

linear = nn.Linear(5, 3)
nn.init.ones_(linear.weight)
print(linear.weight.data)


# 5. 正态分布随机初始化
def test05():

linear = nn.Linear(5, 3)
nn.init.normal_(linear.weight, mean=0, std=1)
print(linear.weight.data)


# 6. kaiming 初始化
def test06():

# kaiming 正态分布初始化
linear = nn.Linear(5, 3)
nn.init.kaiming_normal_(linear.weight, nonlinearity='relu')
print(linear.weight.data)

# kaiming 均匀分布初始化
linear = nn.Linear(5, 3)
nn.init.kaiming_uniform_(linear.weight, nonlinearity='relu')
print(linear.weight.data)


# 7. xavier 初始化
def test07():

# xavier 正态分布初始化
linear = nn.Linear(5, 3)
nn.init.xavier_normal_(linear.weight)
print(linear.weight.data)

# xavier 均匀分布初始化
linear = nn.Linear(5, 3)
nn.init.xavier_uniform_(linear.weight)
print(linear.weight.data)

如何选择参数初始化

  • 激活函数的选择:根据激活函数的类型选择对应的初始化方法

    • Sigmoid/Tanh:xavier 初始化
    • ReLU/Leaky ReLU:kaiming 初始化
  • 神经网络模型的深度

    • 浅层网络:随机初始化即可
    • 深层网络:需要考虑方差平衡,如 xavier 或 kaiming 初始化

神经网络搭建和参数计算

构建神经网络

在pytorch中定义深度神经网络其实就是层堆叠的过程,继承自nn.Module,实现两个方法:

  • __init__方法中定义网络中的层结构,主要是全连接层,并进行初始化
  • forward方法,在调用神经网络模型对象的时候,底层会自动调用该函数。该函数中为初始化定义的layer传入数据,进行前向传播等。

接下来我们来构建如下图所示的神经网络模型:

1755683880282

编码设计如下:

  • 第1个隐藏层:权重初始化采用标准化的xavier初始化 激活函数使用sigmoid
  • 第2个隐藏层:权重初始化采用标准化的He初始化 激活函数采用relu
  • out输出层线性层 假若多分类,采用softmax做数据归一化

构造神经网络模型代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import torch
import torch.nn as nn
from torchsummary import summary # 计算模型参数,查看模型结构, pip install torchsummary -i https://mirrors.aliyun.com/pypi/simple/


# 创建神经网络模型类
class Model(nn.Module):
# 初始化属性值
def __init__(self):
# 调用父类的初始化属性值,确保nn.Module的初始化代码能够正确执行
super(Model, self).__init__()
# 创建第一个隐藏层模型, 3个输入特征,3个输出特征
self.linear1 = nn.Linear(3, 3)
# 初始化权重
nn.init.xavier_normal_(self.linear1.weight)
nn.init.zeros_(self.linear1.bias)
# 创建第二个隐藏层模型, 3个输入特征(上一层的输出特征),2个输出特征
self.linear2 = nn.Linear(3, 2)
# 初始化权重
nn.init.kaiming_normal_(self.linear2.weight, nonlinearity='relu')
nn.init.zeros_(self.linear2.bias)
# 创建输出层模型
self.out = nn.Linear(2, 2)

# 创建前向传播方法, 调用神经网络模型对象时自动执行forward()方法
def forward(self, x):
# 数据经过第一个线性层
x = self.linear1(x)
# 使用sigmoid激活函数
x = torch.sigmoid(x)

# 数据经过第二个线性层
x = self.linear2(x)
# 使用relu激活函数
x = torch.relu(x)

# 数据经过输出层
x = self.out(x)
# 使用softmax激活函数
# dim=-1:每一维度行数据相加为1
x = torch.softmax(x, dim=-1)

return x

训练神经网络模型代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 创建构造模型函数
def train():
# 实例化model对象
my_model = Model()

# 随机产生数据
my_data = torch.randn(5, 3)
print("my_data-->", my_data)
print("my_data shape", my_data.shape)

# 数据经过神经网络模型训练
output = my_model(my_data)
print("output-->", output)
print("output shape-->", output.shape)

# 计算模型参数
# 计算每层每个神经元的w和b个数总和
print("======计算模型参数======")
summary(my_model, input_size=(3,), batch_size=5)

# 查看模型参数
print("======查看模型参数w和b======")
for name, parameter in my_model.named_parameters():
print(name, parameter)


if __name__ == '__main__':
train()

观察数据形状变化

  • 观察程序输入和输出的数据形状变化

    • 输入5行数据,输出也是5行数据
    • 输入5行数据3个特征,经过第一个隐藏层是3个特征,经过第二个隐藏层是2个特征,经过输出层是2个特征
    • 模型最终预测结果是:5行2列数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    mydata.shape---> torch.Size([5, 3])
    output.shape---> torch.Size([5, 2])
    mydata--->
    tensor([[-0.3714, -0.8578, -1.6988],
    [ 0.3149, 0.0142, -1.0432],
    [ 0.5374, -0.1479, -2.0006],
    [ 0.4327, -0.3214, 1.0928],
    [ 2.2156, -1.1640, 1.0289]])
    output--->
    tensor([[0.5095, 0.4905],
    [0.5218, 0.4782],
    [0.5419, 0.4581],
    [0.5163, 0.4837],
    [0.6030, 0.3970]], grad_fn=<SoftmaxBackward>)

模型参数计算

  • 模型参数的计算

    • 以第一个隐层为例:该隐层有3个神经元,每个神经元的参数为:4个(w1,w2,w3,b1),所以一共用3x4=12个参数。

    • 输入数据和网络权重是两个不同的事儿!对于初学者理解这一点十分重要,要分得清。

      1755683907974

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    ----------------------------------------------------------------
    Layer (type) Output Shape Param #
    ================================================================
    Linear-1 [5, 3] 12
    Linear-2 [5, 2] 8
    Linear-3 [5, 2] 6
    ================================================================
    Total params: 26
    Trainable params: 26
    Non-trainable params: 0
    ----------------------------------------------------------------
    Input size (MB): 0.00
    Forward/backward pass size (MB): 0.00
    Params size (MB): 0.00
    Estimated Total Size (MB): 0.00
    ----------------------------------------------------------------

查看模型参数

  • 通常继承nn.Module,撰写自己的网络层。它强大的封装不需要我们定义可学习的参数(比如卷积核的权重和偏置参数)。

  • 如何才能查看封装好的,可学习的网络参数哪?

    • 模块实例名.name_parameters(),会分别返回name和parameter
    1
    2
    3
    4
    5
    6
    7
    8
    # 实例化model对象
    mymodel = Model()

    # 查看网络参数
    for name, parameter in mymodel.named_parameters():
    # print('name--->', name)
    # print('parameter--->', parameter)
    print(name, parameter)

    结果显示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    linear1.weight Parameter containing:
    tensor([[ 0.1715, -0.3711, 0.1692],
    [-0.2497, -0.6156, -0.4235],
    [-0.7090, -0.0380, 0.4790]], requires_grad=True)
    linear1.bias Parameter containing:
    tensor([-0.2320, 0.3431, 0.2771], requires_grad=True)
    linear2.weight Parameter containing:
    tensor([[-0.5044, -0.7435, -0.6736],
    [ 0.6908, -0.1466, -0.0019]], requires_grad=True)
    linear2.bias Parameter containing:
    tensor([0.2340, 0.4730], requires_grad=True)
    out.weight Parameter containing:
    tensor([[ 0.5185, 0.4019],
    [-0.4313, -0.3438]], requires_grad=True)
    out.bias Parameter containing:
    tensor([ 0.4521, -0.6339], requires_grad=True)

损失函数

损失函数概念

在深度学习中, 损失函数是用来==衡量模型参数质量的函数==, 衡量的方式是比较网络输出(预测值)和真实输出(真实值)的差异。

模型通过最小化损失函数的值来调整参数,使其输出更接近真实值。

1755683935950

损失函数在不同的文献中名称是不一样的,主要有以下几种命名方式

1755683942196

损失函数作用:

  • 评估性能:反映模型预测结果与目标值的匹配程度。
  • 指导优化:通过梯度下降等算法最小化损失函数,优化模型参数。

分类任务损失函数

在深度学习的分类任务中使用最多的是交叉熵损失函数,所以在这里我们着重介绍这种损失函数。

多分类任务损失函数

在多分类任务通常使用softmax将logits转换为概率的形式,所以多分类的交叉熵损失也叫做softmax损失,它的计算方法是:

1755683956095

其中:

  • $$y_i$$是样本x属于某一个类别的真实概率
  • 而f(x)是样本属于某一类别的预测分数
  • S是softmax激活函数,将属于某一类别的预测分数转换成概率
  • L用来衡量真实值y和预测值f(x)之间差异性的损失结果

例子:

1755683965918

上图中的交叉熵损失为:

1755683988255

从概率角度理解,我们的目的是最小化正确类别所对应的预测概率的对数的负值(损失值最小),如下图所示:

1755684004095

在PyTorch中使用nn.CrossEntropyLoss()实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch
from torch import nn


# 分类损失函数:交叉熵损失使用nn.CrossEntropyLoss()实现。nn.CrossEntropyLoss()=softmax+损失计算
def test01():
# 设置真实值: 可以是热编码后的结果也可以不进行热编码
# y_true = torch.tensor([[0, 1, 0], [0, 0, 1]], dtype=torch.float32)
# 注意:类型必须是64位整型数据
y_true = torch.tensor([1, 2], dtype=torch.int64)
y_pred = torch.tensor([[0.2, 0.6, 0.2], [0.1, 0.8, 0.1]], requires_grad=True, dtype=torch.float32)
# 实例化交叉熵损失,默认求平均损失
# reduction='sum':总损失
loss = nn.CrossEntropyLoss()
# 计算损失结果
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)

二分类任务损失函数

在处理二分类任务时,我们不再使用softmax激活函数,而是使用sigmoid激活函数,那损失函数也相应的进行调整,使用二分类的交叉熵损失函数:

1755684019401

其中:

  • y是样本x属于某一个类别的真实概率

  • 而$$\hat{y}$$是样本属于某一类别的预测概率

  • L用来衡量真实值y与预测值$$\hat{y}$$之间差异性的损失结果。

    1755684023854

在PyTorch中实现时使用nn.BCELoss() 实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
from torch import nn


def test02():
# 1 设置真实值和预测值
y_true = torch.tensor([0, 1, 0], dtype=torch.float32)
# 预测值是sigmoid输出的结果
y_pred = torch.tensor([0.6901, 0.5459, 0.2469], requires_grad=True)
# 2 实例化二分类交叉熵损失
loss = nn.BCELoss()
# 3 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)

回归任务损失函数

MAE损失函数

**mean absolute loss(MAE)**也被称为L1 Loss,是以绝对误差作为距离

损失函数公式:

1755684034095

曲线如下图所示:

1755684044770

特点是:

  • 由于L1 loss具有稀疏性,为了惩罚较大的值,因此常常将其作为正则项添加到其他loss中作为约束。(0点不可导, 产生稀疏矩阵)
  • L1 loss的最大问题是梯度在零点不平滑,导致会跳过极小值
  • 适用于回归问题中存在异常值或噪声数据时,可以减少对离群点的敏感性

在PyTorch中使用nn.L1Loss()实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
from torch import nn


# 计算inputs与target之差的绝对值
def test03():
# 1 设置真实值和预测值
y_pred = torch.tensor([1.0, 1.0, 1.9], requires_grad=True)
y_true = torch.tensor([2.0, 2.0, 2.0], dtype=torch.float32)
# 2 实例MAE损失对象
loss = nn.L1Loss()
# 3 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)

MSE损失函数

**Mean Squared Loss/ Quadratic Loss(MSE loss)**也被称为L2 loss,或欧氏距离,它以误差的平方和的均值作为距离

损失函数公式:

1755684058375

曲线如下图所示:

1755684071261

特点是:

  • L2 loss也常常作为正则项,对于离群点(outliers)敏感,因为平方项会放大大误差

  • 当预测值与目标值相差很大时, 梯度容易爆炸

    • 梯度爆炸:网络层之间的梯度(值大于1.0)重复相乘导致的指数级增长会产生梯度爆炸
  • 适用于大多数标准回归问题,如房价预测、温度预测等

在PyTorch中通过nn.MSELoss()实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
from torch import nn


def test04():
# 1 设置真实值和预测值
y_pred = torch.tensor([1.0, 1.0, 1.9], requires_grad=True)
y_true = torch.tensor([2.0, 2.0, 2.0], dtype=torch.float32)
# 2 实例MSE损失对象
loss = nn.MSELoss()
# 3 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('myloss:', my_loss)

Smooth L1损失函数

smooth L1说的是光滑之后的L1,是一种结合了均方误差(MSE)和平均绝对误差(MAE)优点的损失函数。它在误差较小时表现得像 MSE,在误差较大时则更像 MAE。

Smooth L1损失函数如下式所示:

1755684088906

其中:𝑥=f(x)−y 为真实值和预测值的差值。

1755684096986

从上图中可以看出,该函数实际上就是一个分段函数

  • 在[-1,1]之间实际上就是L2损失,这样解决了L1的不光滑问题
  • 在[-1,1]区间外,实际上就是L1损失,这样就解决了离群点梯度爆炸的问题

特点是:

  • 对离群点更加鲁棒:当误差较大时,损失函数会线性增加(而不是像MSE那样平方增加),因此它对离群点的惩罚更小,避免了MSE对离群点过度敏感的问题

  • 计算梯度时更加平滑:与MAE相比,Smooth L1在小误差时表现得像MSE,避免了在训练过程中因使用绝对误差而导致的梯度不连续问题

在PyTorch中使用nn.SmoothL1Loss()计算该损失,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
from torch import nn


def test05():
# 1 设置真实值和预测值
y_true = torch.tensor([0, 3])
y_pred = torch.tensor([0.6, 0.4], requires_grad=True)
# 2 实例smmothL1损失对象
loss = nn.SmoothL1Loss()
# 3 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)

神经网络优化方法

多层神经网络的学习能力比单层网络强得多。想要训练多层网络,需要更强大的学习算法。误差反向传播算法(Back Propagation)是其中最杰出的代表,它是目前最成功的神经网络学习算法。现实任务使用神经网络时,大多是在使用 BP 算法进行训练,值得指出的是 BP 算法不仅可用于多层前馈神经网络,还可以用于其他类型的神经网络。通常说 BP 网络时,一般是指用 BP 算法训练的多层前馈神经网络。

这就需要了解两个概念:

  1. 正向传播:指的是数据通过网络从输入层到输出层的传递过程。这个过程的目的是计算网络的输出值(预测值),从而与目标值(真实值)比较以计算误差。
  2. 反向传播:指的是计算损失函数相对于网络中各参数(权重和偏置)的梯度,指导优化器更新参数,从而使神经网络的预测更接近目标值。

梯度下降算法回顾

梯度下降法简单来说就是一种寻找使损失函数最小化的方法

从数学角度来看,梯度的方向是函数增长速度最快的方向,那么梯度的反方向就是函数减少最快的方向,所以有:

1755684118951

其中,η是学习率,如果学习率太小,那么每次训练之后得到的效果都太小,增大训练的时间成本。如果,学习率太大,那就有可能直接跳过最优解,进入无限的训练中。解决的方法就是,学习率也需要随着训练的进行而变化。

1755684129771

在上图中我们展示了一维和多维的损失函数,损失函数呈碗状。在训练过程中损失函数对权重的偏导数就是损失函数在该位置点的梯度。我们可以看到,沿着负梯度方向移动,就可以到达损失函数底部,从而使损失函数最小化。这种利用损失函数的梯度迭代地寻找最小值的过程就是梯度下降的过程。

在进行模型训练时,有三个基础的概念:

  1. Epoch: 使用全部数据对模型进行以此完整训练,训练次数
  2. Batch: 使用训练集中的小部分样本对模型权重进行以此反向传播的参数更新,每次训练每批次样本数量
  3. Iteration: 使用一个 Batch 数据对模型进行一次参数更新的过程,每次训练批次数

假设数据集有 50000 个训练样本,现在选择 Batch Size = 256 对模型进行训练。
每个 Epoch 要训练的图片数量:50000
训练集具有的 Batch 个数:50000/256+1=196
每个 Epoch 具有的 Iteration 个数:196
10个 Epoch 具有的 Iteration 个数:1960

在深度学习中,梯度下降的几种方式的根本区别就在于Batch Size不同,如下表所示:

==注:上表中 Mini-Batch 的 Batch 个数为 N / B + 1 是针对未整除的情况。整除则是 N / B。==

反向传播(BP算法)

利用反向传播算法对神经网络进行训练。该方法与梯度下降算法相结合,对网络中所有权重计算损失函数的梯度,并利用梯度值来更新权值以最小化损失函数。

反向传播概念

前向传播:指的是数据输入到神经网络中,逐层向前传输,一直运算到输出层为止。

反向传播(Back Propagation):利用损失函数ERROR值,从后往前,结合梯度下降算法,依次求各个参数的偏导,并进行参数更新。

1755684156012

在网络的训练过程中经过前向传播后得到的最终结果跟训练样本的真实值总是存在一定误差,这个误差便是损失函数 ERROR。想要减小这个误差,就用损失函数 ERROR,从后往前,依次求各个参数的偏导,这就是反向传播(Back Propagation)

反向传播详解

反向传播算法利用链式法则对神经网络中的各个节点的权重进行更新

【举个栗子🌰:】

如下图是一个简单的神经网络用来举例:激活函数为sigmoid

1755684171177

前向传播运算

1755684185561

接下来是反向传播(求网络误差对各个权重参数的梯度):

我们先来求最简单的,求误差E对w5的导数。首先明确这是一个链式法则的求导过程,要求误差E对w5的导数,需要先求误差E对$$out_{o1}$$的导数,再求$$out_{o1}$$对$$net_{o1}$$的导数,最后再求$$net_{o1}$$对$$w_5$$的导数,经过这个链式法则,我们就可以求出误差E对$$w_5$$的导数(偏导),如下图所示:

1755684201302

导数(梯度)已经计算出来了,下面就是反向传播与参数更新过程

1755684215237

如果要想求误差E对w1的导数,误差E对w1的求导路径不止一条,这会稍微复杂一点,但换汤不换药,计算过程如下所示:

1755684227275

至此,反向传播算法的过程就讲完了啦!

梯度下降优化方法

梯度下降优化算法中,可能会碰到以下情况:

  • 碰到平缓区域,梯度值较小,参数优化变慢
  • 碰到 “鞍点” ,梯度为0,参数无法优化
  • 碰到局部最小值,参数不是最优

对于这些问题, 出现了一些对梯度下降算法的优化方法,例如:MomentumAdaGradRMSpropAdam

1755684253952

指数加权平均

我们最常见的算数平均指的是将所有数加起来除以数的个数,每个数的权重是相同的。指数加权平均指的是给每个数赋予不同的权重求得平均数。移动平均数,指的是计算最近邻的 N 个数来获得平均数。

指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)。

比如:明天气温怎么样,和昨天气温有很大关系,而和一个月前的气温关系就小一些。

计算公式可以用下面的式子来表示:

1755684262692

  • $S_t$ 表示指数加权平均值;
  • $Y_t​$ 表示t时刻的值;
  • β 调节权重系数,该值越大平均数越平缓。

第100天的指数加权平均值为:

1755684273044

下面通过代码来看结果,随机产生 30 天的气温数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import torch
import matplotlib.pyplot as plt


ELEMENT_NUMBER = 30


# 1. 实际平均温度
def test01():

# 固定随机数种子
torch.manual_seed(0)
# 产生30天的随机温度
temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
print(temperature)
# 绘制平均温度
days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
plt.plot(days, temperature, color='r')
plt.scatter(days, temperature)
plt.show()


# 2. 指数加权平均温度
def test02(beta=0.9):

# 固定随机数种子
torch.manual_seed(0)
# 产生30天的随机温度
temperature = torch.randn(size=[ELEMENT_NUMBER,]) * 10
print(temperature)

exp_weight_avg = []
# idx从1开始
for idx, temp in enumerate(temperature, 1):
# 第一个元素的 EWA 值等于自身
if idx == 1:
exp_weight_avg.append(temp)
continue
# 第二个元素的 EWA 值等于上一个 EWA 乘以 β + 当前气温乘以 (1-β)
# idx-2:2-2=0,exp_weight_avg列表中第一个值的下标值
new_temp = exp_weight_avg[idx - 2] * beta + (1 - beta) * temp
exp_weight_avg.append(new_temp)

days = torch.arange(1, ELEMENT_NUMBER + 1, 1)
plt.plot(days, exp_weight_avg, color='r')
plt.scatter(days, temperature)
plt.show()


if __name__ == '__main__':
test01()
test02(0.5)
test02(0.9)

1755684287673

从程序运行结果可以看到:

  • 指数加权平均绘制出的气温变化曲线更加==平缓==
  • β 的值越大,则绘制出的折线==越加平缓,波动越小==(1-β越小,t时刻的$S_t$越不依赖$Y_t$的值)
  • β 值一般默认都是 0.9

动量算法Momentum

当梯度下降碰到 “峡谷” 、”平缓”、”鞍点” 区域时, 参数更新速度变慢。 Momentum 通过指数加权平均法,累计历史梯度值,进行参数更新,越近的梯度值对当前参数更新的重要性越大。

梯度计算公式

​ $$s_t=βs_{t−1}+(1−β)g_t$$

参数更新公式

​ $$w_t=w_{t−1}−ηs_t$$

$s_t$是当前时刻指数加权平均梯度值

$s_{t-1}$是历史指数加权平均梯度值

$g_t$是当前时刻的梯度值

β 是调节权重系数,通常取 0.9 或 0.99

η是学习率

$w_t$是当前时刻模型权重参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
咱们举个例子,假设:权重 β 为 0.9,例如:
第一次梯度值:s1 = g1 = w1
第二次梯度值:s2 = 0.9*s1 + g2*0.1
第三次梯度值:s3 = 0.9*s2 + g3*0.1
第四次梯度值:s4 = 0.9*s3 + g4*0.1
1. w 表示初始梯度
2. g 表示当前轮数计算出的梯度值
3. s 表示历史梯度移动加权平均值

梯度下降公式中梯度的计算,就不再是当前时刻t的梯度值,而是历史梯度值的指数移动加权平均值。
公式修改为:
Wt = Wt-1 - η*St
Wt:当前时刻模型权重参数
St:当前时刻指数加权平均梯度值
η:学习率

Monmentum 优化方法是如何一定程度上克服 “平缓”、”鞍点”、”峡谷” 的问题呢?

1755684301747

  • 当处于鞍点位置时,由于当前的梯度为 0,参数无法更新。但是 Momentum 动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。
  • 由于 mini-batch 普通的梯度下降算法,每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum 使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程。一定程度上有利于降低 “峡谷” 问题的影响。

在pytorch中动量梯度优化法编程实践如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test01():
# 1 初始化权重参数
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()
# 2 实例化优化方法:SGD 指定参数beta=0.9
optimizer = torch.optim.SGD([w], lr=0.01, momentum=0.9)
# 3 第1次更新 计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4 第2次更新 计算梯度,并对参数进行更新
# 使用更新后的参数机选输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

结果显示:

1
2
1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000
2次: 梯度w.grad: 0.990000, 更新后的权重:0.971100

AdaGrad

AdaGrad 通过对不同的参数分量使用不同的学习率,AdaGrad 的学习率总体会逐渐减小,这是因为 AdaGrad 认为:在起初时,我们距离最优目标仍较远,可以使用较大的学习率,加快训练速度,随着迭代次数的增加,学习率逐渐下降。

其计算步骤如下:

  1. 初始化学习率 η、初始化参数w、小常数 σ = 1e-10

  2. 初始化梯度累计变量 s = 0

  3. 从训练集中采样 m 个样本的小批量,计算梯度$g_t​$

  4. 累积平方梯度: $s_t​$ = $s_{t-1}​$ + $g_t​$ ⊙ $g_t​$,⊙ 表示各个分量相乘

  5. 学习率 η 的计算公式如下:

    ​ η = $$η\over\sqrt{s_t}+σ$$

  6. 权重参数更新公式如下:

    ​ $w_t$ = $$w_{t-1}$$ - $$η\over\sqrt{s_t}+σ$$ * $g_t$

  7. 重复 3-7 步骤

AdaGrad 缺点是可能会使得学习率过早、过量的降低,导致模型训练后期学习率太小,较难找到最优解。

在PyTorch中AdaGrad优化法编程实践如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test02():
# 1 初始化权重参数
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()
# 2 实例化优化方法:adagrad优化方法
optimizer = torch.optim.Adagrad([w], lr=0.01)
# 3 第1次更新 计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4 第2次更新 计算梯度,并对参数进行更新
# 使用更新后的参数机选输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

结果显示:

1
2
1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000
2次: 梯度w.grad: 0.990000, 更新后的权重:0.982965

RMSProp

RMSProp 优化算法是对 AdaGrad 的优化。最主要的不同是,其使用指数加权平均梯度替换历史梯度的平方和。

其计算过程如下:

  1. 初始化学习率 η、初始化权重参数w、小常数 σ = 1e-10

  2. 初始化梯度累计变量 s = 0

  3. 从训练集中采样 m 个样本的小批量,计算梯度 $g_t$

  4. 使用指数加权平均累计历史梯度,⊙ 表示各个分量相乘,公式如下:

    ​ $s_t$ = β$s_{t-1}$ + (1-β)$g_t$⊙$g_t$

  5. 学习率 η 的计算公式如下:

    ​ η = $η\over\sqrt{s_t}+σ​$

  6. 权重参数更新公式如下:

    ​ $w_t$ = $w_{t-1}$ - $η\over\sqrt{s_t}+σ$ * $g_t$

  7. 重复 3-7 步骤

RMSProp 与 AdaGrad 最大的区别是对梯度的累积方式不同,对于每个梯度分量仍然使用不同的学习率。

RMSProp 通过引入衰减系数β,控制历史梯度对历史梯度信息获取的多少. 被证明在神经网络非凸条件下的优化更好,学习率衰减更加合理一些。

需要注意的是:AdaGrad 和 RMSProp 都是对于不同的参数分量使用不同的学习率,如果某个参数分量的梯度值较大,则对应的学习率就会较小,如果某个参数分量的梯度较小,则对应的学习率就会较大一些。

在PyTorch中RMSprop梯度优化法,编程实践如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test03():
# 1 初始化权重参数
w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
loss = ((w ** 2) / 2.0).sum()
# 2 实例化优化方法:RMSprop算法,其中alpha对应beta
optimizer = torch.optim.RMSprop([w], lr=0.01, alpha=0.9)
# 3 第1次更新 计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4 第2次更新 计算梯度,并对参数进行更新
# 使用更新后的参数机选输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

结果显示:

1
2
1次: 梯度w.grad: 1.000000, 更新后的权重:0.968377
2次: 梯度w.grad: 0.968377, 更新后的权重:0.945788

Adam

  • Momentum 使用指数加权平均计算当前的梯度值

  • AdaGrad、RMSProp 使用自适应的学习率

  • Adam优化算法(Adaptive Moment Estimation,自适应矩估计)将 Momentum 和 RMSProp 算法结合在一起

    • 修正梯度: 使⽤梯度的指数加权平均
    • 修正学习率: 使⽤梯度平⽅的指数加权平均
  • 原理:Adam 是结合了 MomentumRMSProp 优化算法的优点的自适应学习率算法。它计算了梯度的一阶矩(平均值)和二阶矩(梯度的方差)的自适应估计,从而动态调整学习率。

  • 梯度计算公式

    ​ $$m_t=β_1m_{t−1}+(1−β_1)g_t​$$

    ​ $$s_t=β_2s_{t−1}+(1−β_2)gt^2​$$

    ​ $$\hat{m_t}​$$ = $$m_t\over1−β_1^t​$$, $$\hat{s_t}​$$=$$s_t\over1−β_2^t​$$

  • 权重参数更新公式:

    ​ $$w_t​$$ = $$w_{t−1}​$$ − $$η\over\sqrt{\hat{s_t}}+ϵ​$$$\hat{m_t}​$

其中,$$m_t$$ 是梯度的一阶矩估计,$$s_t$$ 是梯度的二阶矩估计,$$ \hat{m_t}$$和 $$\hat{s_t}$$ 是偏差校正后的估计。

在PyTroch中,Adam梯度优化法编程实践如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test04():
# 1 初始化权重参数
w = torch.tensor([1.0], requires_grad=True)
loss = ((w ** 2) / 2.0).sum()
# 2 实例化优化方法:Adam算法,其中betas是指数加权的系数
optimizer = torch.optim.Adam([w], lr=0.01, betas=[0.9, 0.99])
# 3 第1次更新 计算梯度,并对参数进行更新
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))
# 4 第2次更新 计算梯度,并对参数进行更新
# 使用更新后的参数机选输出结果
loss = ((w ** 2) / 2.0).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

结果显示:

1
2
1次: 梯度w.grad: 1.000000, 更新后的权重:0.990000 
2次: 梯度w.grad: 0.990000, 更新后的权重:0.980003

小结

优化算法 优点 缺点 适用场景
SGD 简单、容易实现。 收敛速度较慢,容易震荡,特别是在复杂问题中。 用于简单任务,或者当数据特征分布相对稳定时。
Momentum 可以加速收敛,减少震荡,特别是在高曲率区域。 需要手动调整动量超参数,可能会在小步长训练中过度更新。 用于非平稳优化问题,尤其是深度学习中的应用。
AdaGrad 自适应调整学习率,适用于稀疏数据。 学习率会在训练过程中逐渐衰减,可能导致早期停滞。 适合稀疏数据,如 NLP 或推荐系统中的特征。
RMSProp 解决了 AdaGrad 学习率过早衰减的问题,适应性强。 需要选择合适的超参数,更新可能会过于激进。 适用于动态问题、非平稳目标函数,如深度学习训练。
Adam 结合了 Momentum 和 RMSProp 的优点,适应性强且稳定。 需要调节更多的超参数,训练过程中可能会产生较大波动。 广泛适用于各种深度学习任务,特别是非平稳和复杂问题。
  • 简单任务和较小的模型:SGD 或 Momentum

  • 复杂任务或有大量数据:Adam 是最常用的选择,因其在大部分任务上都表现优秀

  • 需要处理稀疏数据或文本数据:Adagrad 或 RMSProp

学习率衰减优化方法

为什么要进行学习率优化

在训练神经网络时,一般情况下学习率都会随着训练而变化。这主要是由于,在神经网络训练的后期,如果学习率过高,会造成loss的振荡,但是如果学习率减小的过慢,又会造成收敛变慢的情况。

运行下面代码,观察学习率设置不同对网络训练的影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# x看成是权重,y看成是loss,下面通过代码来理解学习率的作用
def func(x_t):
return torch.pow(2*x_t, 2) # y = 4 x ^2

# 采用较小的学习率,梯度下降的速度慢
# 采用较大的学习率,梯度下降太快越过了最小值点,导致不收敛,甚至震荡
def test():

x = torch.tensor([2.], requires_grad=True)
# 记录loss迭代次数,画曲线
iter_rec, loss_rec, x_rec = list(), list(), list()

# 实验学习率: 0.01 0.02 0.03 0.1 0.2 0.3 0.4
# lr = 0.1 # 正常的梯度下降
# lr = 0.125 # 当学习率设置0.125 一下子求出一个最优解
# x=0 y=0 在x=0处梯度等于0 x的值x=x-lr*x.grad就不用更新了
# 后续再多少次迭代 都固定在最优点

lr = 0.2 # x从2.0一下子跨过0点,到了左侧负数区域
# lr = 0.3 # 梯度越来越大 梯度爆炸
max_iteration = 4
for i in range(max_iteration):
y = func(x) # 得出loss值
y.backward() # 计算x的梯度
print("Iter:{}, X:{:8}, X.grad:{:8}, loss:{:10}".format(
i, x.detach().numpy()[0], x.grad.detach().numpy()[0], y.item()))
x_rec.append(x.item()) # 梯度下降点 列表
# 更新参数
x.data.sub_(lr * x.grad) # x = x - x.grad
x.grad.zero_()
iter_rec.append(i) # 迭代次数 列表
loss_rec.append(y) # 损失值 列表
# 迭代次数-损失值 关系图
plt.subplot(121).plot(iter_rec, loss_rec, '-ro')
plt.grid()
plt.xlabel("Iteration X")
plt.ylabel("Loss value Y")
# 函数曲线-下降轨迹 显示图
x_t = torch.linspace(-3, 3, 100)
y = func(x_t)
plt.subplot(122).plot(x_t.numpy(), y.numpy(), label="y = 4*x^2")
y_rec = [func(torch.tensor(i)).item() for i in x_rec]
print('x_rec--->', x_rec)
print('y_rec--->', y_rec)
# 指定线的颜色和样式(-ro:红色圆圈,b-:蓝色实线等)
plt.subplot(122).plot(x_rec, y_rec, '-ro')
plt.grid()
plt.legend()
plt.show()

运行效果图如下:

可以看出:采用较小的学习率,梯度下降的速度慢;采用较大的学习率,梯度下降太快越过了最小值点,导致震荡,甚至不收敛(梯度爆炸)。

1755684331646

等间隔学习率衰减

等间隔学习率衰减方式如下所示:

1755684345001

在PyTorch中实现时使用:

1
2
3
4
#   step_size:调整间隔数=50
# gamma:调整系数=0.5
# 调整方式:lr = lr * gamma
optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1)

具体使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import torch
from torch import optim
import matplotlib.pyplot as plt


def test_StepLR():
# 0.参数初始化
LR = 0.1 # 设置学习率初始化值为0.1
iteration = 10
max_epoch = 200
# 1 初始化参数
y_true = torch.tensor([0])
x = torch.tensor([1.0])
w = torch.tensor([1.0], requires_grad=True)
# 2.优化器
optimizer = optim.SGD([w], lr=LR, momentum=0.9)
# 3.设置学习率下降策略
scheduler_lr = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.5)
# 4.获取学习率的值和当前的epoch
lr_list, epoch_list = [], []
for epoch in range(max_epoch):
lr_list.append(scheduler_lr.get_last_lr()) # 获取当前lr
epoch_list.append(epoch) # 获取当前的epoch
for i in range(iteration): # 遍历每一个batch数据
loss = (w*x-y_true)**2 # 目标函数
optimizer.zero_grad()
# 反向传播
loss.backward()
optimizer.step()
# 更新下一个epoch的学习率
scheduler_lr.step()
# 5.绘制学习率变化的曲线
plt.plot(epoch_list, lr_list, label="Step LR Scheduler")
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()

指定间隔学习率衰减

指定间隔学习率衰减的效果如下:

1755684358843

在PyTorch中实现时使用:

1
2
3
4
# milestones:设定调整轮次:[50, 125, 160]
# gamma:调整系数
# 调整方式:lr = lr * gamma
optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1)

具体使用方式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import torch
from torch import optim
import matplotlib.pyplot as plt


def test_MultiStepLR():
torch.manual_seed(1)
LR = 0.1
iteration = 10
max_epoch = 200
y_true = torch.tensor([0])
x = torch.tensor([1.0])
w = torch.tensor([1.0], requires_grad=True)
optimizer = optim.SGD([w], lr=LR, momentum=0.9)
# 设定调整时刻数
milestones = [50, 125, 160]
# 设置学习率下降策略
scheduler_lr = optim.lr_scheduler.MultiStepLR(optimizer, milestones=milestones, gamma=0.5)
lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):
lr_list.append(scheduler_lr.get_last_lr())
epoch_list.append(epoch)
for i in range(iteration):
loss = (w*x-y_true)**2
optimizer.zero_grad()
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
# 更新下一个epoch的学习率
scheduler_lr.step()
plt.plot(epoch_list, lr_list, label="Multi Step LR Scheduler\nmilestones:{}".format(milestones))
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()

按指数学习率衰减

按指数衰减调整学习率的效果如下:

1755684370452

在PyTorch中实现时使用:

1
2
3
4
# gamma:指数的底
# 调整方式
# lr= lr∗gamma^epoch
optim.lr_scheduler.ExponentialLR(optimizer, gamma)

具体使用方式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import torch
from torch import optim
import matplotlib.pyplot as plt


def test_ExponentialLR():
# 0.参数初始化
LR = 0.1 # 设置学习率初始化值为0.1
iteration = 10
max_epoch = 200
# 1 初始化参数
y_true = torch.tensor([0])
x = torch.tensor([1.0])
w = torch.tensor([1.0], requires_grad=True)
# 2.优化器
optimizer = optim.SGD([w], lr=LR, momentum=0.9)
# 3.设置学习率下降策略
gamma = 0.95
scheduler_lr = optim.lr_scheduler.ExponentialLR(optimizer, gamma=gamma)
# 4.获取学习率的值和当前的epoch
lr_list, epoch_list = list(), list()
for epoch in range(max_epoch):
lr_list.append(scheduler_lr.get_last_lr())
epoch_list.append(epoch)
for i in range(iteration): # 遍历每一个batch数据
loss = (w*x-y_true)**2
optimizer.zero_grad()
# 反向传播
loss.backward()
optimizer.step()
# 更新下一个epoch的学习率
scheduler_lr.step()
# 5.绘制学习率变化的曲线
plt.plot(epoch_list, lr_list, label="Multi Step LR Scheduler")
plt.xlabel("Epoch")
plt.ylabel("Learning rate")
plt.legend()
plt.show()

小结

方法 等间隔学习率衰减 (Step Decay) 指定间隔学习率衰减 (Exponential Decay) 指数学习率衰减 (Exponential Moving Average Decay)
衰减方式 固定步长衰减 指定步长衰减 平滑指数衰减,历史平均考虑
实现难度 简单易实现 相对简单,容易调整 需要额外历史计算,较复杂
适用场景 大型数据集、较为简单的任务 对训练平稳性要求较高的任务 高精度训练,避免过快收敛
优点 直观,易于调试,适用于大批量数据 易于调试,稳定训练过程 平滑且考虑历史更新,收敛稳定性较强
缺点 学习率变化较大,可能跳过最优点 在某些情况下可能衰减过快,导致优化提前停滞 超参数调节较为复杂,可能需要更多的计算资源

正则化方法

什么是正则化

1755684384611

  • 在设计机器学习算法时希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化
  • 神经网络强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略
  • 目前在深度学习中使用较多的策略有范数惩罚,DropOut,特殊的网络层等,接下来我们对其进行详细的介绍

Dropout正则化

在训练深层神经网络时,由于模型参数较多,在数据量不足的情况下,很容易过拟合。Dropout(中文翻译成随机失活)是一个简单有效的正则化方法。

1755684399217

  • 在训练过程中,Dropout的实现是让神经元以超参数p(丢弃概率)的概率停止工作或者激活被置为0,未被置为0的进行缩放,缩放比例为1/(1-p)。训练过程可以认为是对完整的神经网络的一些子集进行训练,每次基于输入数据只更新子网络的参数
  • 在实际应用中,Dropout参数p的概率通常取值在0.2到0.5之间
    • 对于较小的模型或较复杂的任务,丢弃率可以选择0.3或更小
    • 对于非常深的网络,较大的丢弃率(如0.5或0.6)可能会有效防止过拟合
    • 实际应用中,通常会在全连接层(激活函数后)之后添加Dropout层
  • 在测试过程中,随机失活不起作用
    • 在测试阶段,使用所有的神经元进行预测,以获得更稳定的结果
    • 直接使用训练好的模型进行测试,由于所有的神经元都参与计算,输出的期望值会比训练阶段高。测试阶段的期望输出是 E[x_test] = x
    • 测试/推理模式:==model.eval()==
  • 缩放的必要性
    • 在训练阶段,将参与计算的神经元的输出除以(1-p)
    • 经过Dropout后的期望输出变为 E[x_dropout] = [(1-p) * x] / (1-p) = x,与测试阶段的期望输出一致
    • 训练模型:==model.train()==

我们通过一段代码观察下dropout的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch
import torch.nn as nn

def test():
# 初始化随机失活层
dropout = nn.Dropout(p=0.4)
# 初始化输入数据:表示某一层的weight信息
inputs = torch.randint(0, 10, size=[1, 4]).float()
layer = nn.Linear(4,5)
y = layer(inputs)
y = torch.relu(y)
print("未失活FC层的输出结果:\n", y)
y = dropout(y)
print("失活后FC层的输出结果:\n", y)

输出结果:

1
2
3
4
未失活FC层的输出结果:
tensor([[0.0000, 1.8033, 1.4608, 4.5189, 6.9116]], grad_fn=<ReluBackward0>)
失活后FC层的输出结果:
tensor([[0.0000, 3.0055, 2.4346, 7.5315, 11.5193]], grad_fn=<MulBackward0>)

上述代码将Dropout层的丢弃概率p设置为0.4,此时经过Dropout层计算的张量中就出现了很多0, 未变为0的按照(1/(1-0.4))进行处理。

批量归一正则化(Batch Normalization)

在神经网络的训练过程中,流经网络的数据都是一个batch,每个batch之间的数据分布变化非常剧烈,==这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据==,给模型训练带来非常大的不稳定性,使得模型难以收敛。如果我们对每一个batch的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。

通过标准化每一层的输入,使其均值接近0,方差接近1,从而加速训练并提高泛化能力。

1755684418377

先对数据标准化,再对数据重构(缩放+平移),写成公式如下所示:

1755684433979

λ和β是可学习的参数,它相当于对标准化后的值做了一个线性变换λ为系数,β为偏置;

eps 通常指为 1e-5,避免分母为 0;

E(x) 表示变量的均值;

Var(x) 表示变量的方差;

批量归一化的步骤如下:

  1. 计算均值和方差:对于每个神经元(即每一层的输入特征),计算该特征在一个小批量(batch)上的均值 $$μ_B$$ 和方差 $$\sigma_B^2$$,它们的计算公式如下:

    ​ $$μ_B=\frac{1}{m} \sum_{i=1}^{m} x_i​$$

    ​ $$σ_B^2=\frac{1}{m} \sum_{i=1}^{m} (x_i - \mu_B)^2$$

    其中 $$x_i​$$ 表示小批量中的第 $$i​$$ 个样本,$$m​$$ 是小批量的样本数量。

  2. 标准化:然后,对每个样本的输入进行标准化,得到归一化的输出:

    ​ $$\hat{x}_i = \frac{x_i - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}}​$$

    其中,$$ϵ$$ 是一个小常数,用来避免除以零的情况。

  3. 缩放和平移:为了让网络能够恢复其学习能力,BN 层引入了两个可训练的参数 $$γ$$ 和 $$β$$,分别用于缩放平移

    ​ $$y_i = \gamma \hat{x}_i + β​$$

    其中,$$γ$$ 和 $$β$$ 是可学习的参数,通过 γ 和 β,BN 层不再是简单的将每一层输入强行变为标准正态分布,而是允许网络学习更适合于该层的输入分布;规范化操作会丢失原始输入的一些信息,而 $$γ$$ 和 $$β$$ 可以弥补这种信息损失。

批量归一化的作用:

  • **减少内部协方差偏移:**通过对每层的输入进行标准化,减少了输入数据分布的变化,从而加速了训练过程,并使得网络在训练过程中更加稳定。

  • 加速训练:

    • 在没有批量归一化的情况下,神经网络的训练通常会很慢,尤其是深度网络。因为在每层的训练过程中,输入数据的分布(特别是前几层)会不断变化,这会导致网络学习速度缓慢。
    • 批量归一化通过确保每层的输入数据在训练时分布稳定,有效减少了这种变化,从而加速了训练过程。
  • **起到正则化作用:**批量归一化可以视作一种正则化方法,因为它引入了对训练样本的噪声(不同批次的统计信息不同,批次较小的均值和方差估计会更加不准确),使得模型不容易依赖特定的输入特征,从而起到一定的正则化效果,减少了对其他正则化技术(如Dropout)的需求。

  • **提升泛化能力:**由于其正则化效果,批量归一化能帮助网络在测试集上取得更好的性能。

==批量归一化层在计算机视觉领域使用较多==

Batch Normalization 的使用步骤:

  1. 在网络层后添加 BN 层:
    • 通常,BN 层会添加在卷积层 (Conv2d) 或全连接层 (Linear) 之后,激活函数之前
    • 例如:Conv2d -> BN -> ReLU 或者 Linear -> BN -> ReLU。
  2. 训练时:==model.train()==
    • BN 层会计算当前批次的均值 $$μ$$ 和方差 $$σ²$$。
    • 然后,利用这两个统计量对当前批次的数据进行规范化。
    • 规范化后的数据会被缩放 $$γ$$ 和平移 $$β$$。
    • 同时,BN 层还会维护一个全局均值全局方差的移动平均值,用于推理阶段。
  3. 推理时:==model.eval()==
    • 推理时,不会再使用当前批次的均值和方差,而是使用训练阶段计算的全局均值全局方差
    • 同样,规范化后的数据会被缩放 $$γ$$ 和平移 $$β$$。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import torch
import torch.nn as nn

"""
BatchNorm1d:主要应用于全连接层或处理一维数据的网络,例如文本处理。它接收形状为 (N, num_features) 的张量作为输入。
BatchNorm2d:主要应用于卷积神经网络,处理二维图像数据或特征图。它接收形状为 (N, C, H, W) 的张量作为输入。
BatchNorm3d:主要用于三维卷积神经网络 (3D CNN),处理三维数据,例如视频或医学图像。它接收形状为 (N, C, D, H, W) 的张量作为输入。
"""

def tes01():
# 创建测试样本, 假设是经过卷积层(Conv2d)处理后的特征图
# (N, C, H, W): 一张图, 两个通道, 每个通道3行4列
# 可以创建1个样本, 图像的BN是对每个通道的特征图(行列数据)进行标准化
input_2d = torch.randn(size=(1, 2, 3, 4))
print("input-->", input_2d)
# num_features:输入特征数
# eps:非常小的浮点数,防止除以0的错误
# momentum:动量系数
# affine:默认为True,γ和β被使用,让BN层更加灵活
bn2d = nn.BatchNorm2d(num_features=2, eps=1e-05, momentum=0.1, affine=True)
output = bn2d(input_2d)
print("output-->", output)
print(output.size())

print(bn2d.weight)
print(bn2d.bias)

def tes02():
# 创建测试样本
# 2个样本, 1个特征
# 不能创建1个样本, 无法统计均值和方差
input_1d = torch.randn(size=(2, 2))
# 创建线性层对象
linear1 = nn.Linear(in_features=2, out_features=3)
# 创建BN层对象
# num_features:输入特征数
bn1d = nn.BatchNorm1d(num_features=3) # 20 output features
output_1d = linear1(input_1d)
# 进行批量归一化
output = bn1d(output_1d)
print("output-->", output)
print(output.size()) # (32, 20)

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
test01:
input_2d--> tensor([[[[-0.2751, -1.2183, -0.5106, -0.1540],
[-0.4585, -0.5989, -0.6063, 0.5986],
[-0.4745, 0.1496, -1.1266, -1.2377]],

[[ 0.2580, 1.2065, 1.4598, 0.8387],
[-0.4586, 0.8938, -0.3328, 0.1192],
[-0.3265, -0.6263, 0.0419, -1.2231]]]])
output--> tensor([[[[ 0.4164, -1.3889, -0.0343, 0.6484],
[ 0.0655, -0.2032, -0.2175, 2.0889],
[ 0.0349, 1.2294, -1.2134, -1.4262]],

[[ 0.1340, 1.3582, 1.6853, 0.8835],
[-0.7910, 0.9546, -0.6287, -0.0452],
[-0.6205, -1.0075, -0.1449, -1.7779]]]],
grad_fn=<NativeBatchNormBackward0>)
torch.Size([1, 2, 3, 4])
Parameter containing:
tensor([1., 1.], requires_grad=True)
Parameter containing:
tensor([0., 0.], requires_grad=True)

test02:
output--> tensor([[-0.9998, 1.0000, 1.0000],
[ 0.9998, -1.0000, -1.0000]], grad_fn=<NativeBatchNormBackward0>)
torch.Size([2, 3])

手机价格分类案例

案例需求分析

小明创办了一家手机公司,他不知道如何估算手机产品的价格。为了解决这个问题,他收集了多家公司的手机销售数据。该数据为二手手机的各个性能的数据,最后根据这些性能得到4个价格区间,作为这些二手手机售出的价格区间。主要包括:

1755684460653

我们需要帮助小明找出手机的功能(例如:RAM等)与其售价之间的某种关系。我们可以使用机器学习的方法来解决这个问题,也可以构建一个全连接的网络。
需要注意的是: 在这个问题中,我们不需要预测实际价格,而是一个价格范围,它的范围使用 0、1、2、3 来表示,所以该问题也是一个分类问题。接下来我们还是按照四个步骤来完成这个任务:

  • 准备训练集数据
  • 构建要使用的模型
  • 模型训练
  • 模型预测评估

构建数据集

数据共有 2000 条, 其中 1600 条数据作为训练集, 400 条数据用作测试集。 我们使用 sklearn 的数据集划分工作来完成。并使用 PyTorch 的 TensorDataset 来将数据集构建为 Dataset 对象,方便构造数据集加载对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 导入相关模块
import torch
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
import torch.nn as nn
from torchsummary import summary
import torch.optim as optim
from sklearn.model_selection import train_test_split
import numpy as np
import pandas as pd
import time

# 构建数据集
def create_dataset():
# 使用pandas读取数据
data = pd.read_csv('data/手机价格预测.csv')
# 特征值和目标值
x, y = data.iloc[:, :-1], data.iloc[:, -1]
# 类型转换:特征值
x = x.astype(np.float32)
# 数据集划分
x_train, x_valid, y_train, y_valid = train_test_split(x, y, train_size=0.8, random_state=88)
# 构建数据集,转换为pytorch的形式
train_dataset = TensorDataset(torch.from_numpy(x_train.values), torch.tensor(y_train.values))
valid_dataset = TensorDataset(torch.from_numpy(x_valid.values), torch.tensor(y_valid.values))
# 返回结果
# x_train.shape[1]: 特征数
# len(np.unique(y)): 类别数
return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))


if __name__ == '__main__':
# 获取数据
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
print("输入特征数:", input_dim)
print("分类个数:", class_num)

输出结果为:

1
2
输入特征数: 20 
分类个数: 4

构建分类网络模型

构建全连接神经网络来进行手机价格分类,该网络主要由三个线性层来构建,使用relu激活函数。
网络共有 3 个全连接层, 具体信息如下:
第一层: 输入为维度为 20, 输出维度为: 128
第二层: 输入为维度为 128, 输出维度为: 256
第三层: 输入为维度为 256, 输出维度为: 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 构建网络模型
class PhonePriceModel(nn.Module):
def __init__(self, input_dim, output_dim):
super(PhonePriceModel, self).__init__()
# 1. 第一层: 输入为维度为 20, 输出维度为: 128
self.linear1 = nn.Linear(input_dim, 128)
# 2. 第二层: 输入为维度为 128, 输出维度为: 256
self.linear2 = nn.Linear(128, 256)
# 3. 第三层: 输入为维度为 256, 输出维度为: 4
self.linear3 = nn.Linear(256, output_dim)

def forward(self, x):
# 前向传播过程
x = torch.relu(self.linear1(x))
x = torch.relu(self.linear2(x))
# 后续CrossEntropyLoss损失函数中包含softmax过程, 所以当前步骤不进行softmax操作
output = self.linear3(x)
# 获取数据结果
return output


if __name__ == '__main__':
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
# 模型实例化
model = PhonePriceModel(input_dim, class_num)
summary(model, input_size=(input_dim,), batch_size=16)

1755684526564

模型训练

网络编写完成之后,我们需要编写训练函数。所谓的训练函数,指的是输入数据读取、送入网络、计算损失、更新参数的流程,该流程较为固定。我们使用的是多分类交叉生损失函数、使用 SGD 优化方法。最终,将训练好的模型持久化到磁盘中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 模型训练过程
def train(train_dataset,input_dim,class_num):
# 固定随机数种子
torch.manual_seed(0)
# 初始化数据加载器
dataloader = DataLoader(train_dataset, shuffle=True, batch_size=8)
# 初始化模型
model = PhonePriceModel(input_dim, class_num)
# 损失函数 CrossEntropyLoss = softmax + 损失计算
criterion = nn.CrossEntropyLoss()
# 优化方法
optimizer = optim.SGD(model.parameters(), lr=1e-3)
# 训练轮数
num_epoch = 50
# 遍历每个轮次的数据
for epoch_idx in range(num_epoch):
# 训练时间
start = time.time()
# 计算损失
total_loss = 0.0
total_num = 0
# 遍历每个batch数据进行处理
for x, y in dataloader:
# 将数据送入网络中进行预测
model.train() # 使用训练模式
output = model(x)
# 计算损失
loss = criterion(output, y)
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
# 损失计算
total_num += 1
total_loss += loss.item()
# 打印损失变换结果
print('epoch: %4s loss: %.2f, time: %.2fs' %(epoch_idx + 1, total_loss / total_num, time.time() - start))
# 模型保存
# state_dict(): 将模型的参数保存到字典中
torch.save(model.state_dict(), 'model/phone-price-model.pth')


if __name__ == '__main__':
# 获取数据
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
# 模型训练过程
train(train_dataset,input_dim,class_num)

1755684506387

模型评估

使用训练好的模型,对未知的样本的进行预测的过程。我们这里使用前面单独划分出来的验证集来进行评估。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def test(valid_dataset, input_dim, class_num):
# 加载模型和训练好的网络参数
model = PhonePriceModel(input_dim, class_num)
# load_state_dict:将加载的参数字典应用到模型上
# load:加载用来保存模型参数的文件
model.load_state_dict(torch.load('model/phone-price-model.pth'))
# 构建加载器
dataloader = DataLoader(valid_dataset, batch_size=8, shuffle=False)
# 评估测试集
correct = 0
# 遍历测试集中的数据
for x, y in dataloader:
# 将其送入网络中
model.eval() # 使用推理模式
output = model(x)
# 获取类别结果
# argmax: 最大值对应的下标, 即类别编码
y_pred = torch.argmax(output, dim=1)
# 获取预测正确的个数
correct += (y_pred == y).sum()
# 求预测精度
print('Acc: %.5f' % (correct.item() / len(valid_dataset)))


if __name__ == '__main__':
# 获取数据
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
# 模型预测结果
test(valid_dataset, input_dim, class_num)

输出结果:

1
Acc: 0.64250

网络性能优化

我们前面的网络模型在测试集的准确率为: 0.64250, 我们可以通过以下方面进行调优:

  1. 对输入数据进行标准化
  2. 调整优化方法
  3. 调整学习率
  4. 增加批量归一化层
  5. 增加网络层数、神经元个数
  6. 增加训练轮数
  7. 等等…

进行下如下调整:

  1. 优化方法由 SGD 调整为 Adam
  2. 学习率由 1e-3 调整为 1e-4
  3. 对数据进行标准化
  4. 增加网络深度, 即: 增加网络参数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import torch
import torch.nn as nn
import pandas as pd
from sklearn.model_selection import train_test_split
from torch.utils.data import TensorDataset
from torch.utils.data import DataLoader
import torch.optim as optim
import numpy as np
import time
from sklearn.preprocessing import StandardScaler


# 构建数据集
def create_dataset():
# 使用pandas读取数据
data = pd.read_csv('./data/手机价格预测.csv')
# 特征值和目标值
x, y = data.iloc[:, :-1], data.iloc[:, -1]
# 类型转换:特征值,目标值
x = x.astype(np.float32)
y = y.astype(np.int64)
# 数据集划分
x_train, x_valid, y_train, y_valid = train_test_split(x, y, train_size=0.8, random_state=88, stratify=y)
# 优化①:数据标准化
transfer = StandardScaler()
x_train = transfer.fit_transform(x_train)
x_valid = transfer.transform(x_valid)
# 构建数据集,转换为pytorch的形式
train_dataset = TensorDataset(torch.from_numpy(x_train), torch.tensor(y_train.values))
valid_dataset = TensorDataset(torch.from_numpy(x_valid), torch.tensor(y_valid.values))
# 返回结果
return train_dataset, valid_dataset, x_train.shape[1], len(np.unique(y))


# 构建网络模型
class PhonePriceModel(nn.Module):

def __init__(self, input_dim, output_dim):
super(PhonePriceModel, self).__init__()
# 优化②:增加网络深度
# 1. 第一层: 输入为维度为 20, 输出维度为: 128
self.linear1 = nn.Linear(input_dim, 128)
# 2. 第二层: 输入为维度为 128, 输出维度为: 256
self.linear2 = nn.Linear(128, 256)
# 3. 第三层: 输入为维度为 256, 输出维度为: 512
self.linear3 = nn.Linear(256, 512)
# 4. 第三层: 输入为维度为 512, 输出维度为: 128
self.linear4 = nn.Linear(512, 128)
# 5. 第三层: 输入为维度为 128, 输出维度为: 4
self.linear5 = nn.Linear(128, output_dim)

def forward(self, x):
# 前向传播过程
x = torch.relu(self.linear1(x))
x = torch.relu(self.linear2(x))
x = torch.relu(self.linear3(x))
x = torch.relu(self.linear4(x))
# 后续CrossEntropyLoss损失函数中包含softmax过程, 所以当前步骤不进行softmax操作
output = self.linear5(x)
# 获取数据结果
return output


# 编写训练函数
def train(train_dataset, input_dim, class_num):
# 固定随机数种子
torch.manual_seed(0)
# 初始化数据加载器
dataloader = DataLoader(train_dataset, shuffle=True, batch_size=8)
# 初始化模型
model = PhonePriceModel(input_dim, class_num)
# 损失函数 CrossEntropyLoss = softmax + 损失计算
criterion = nn.CrossEntropyLoss()
# 优化③:使用Adam优化方法, 优化④:学习率变为1e-4
optimizer = optim.Adam(model.parameters(), lr=1e-4)
# 遍历每个轮次的数据
num_epoch = 50
for epoch_idx in range(num_epoch):
# 训练时间
start = time.time()
# 计算损失
total_loss = 0.0
total_num = 0
# 遍历每个batch数据进行处理
for x, y in dataloader:
model.train()
output = model(x)
# 计算损失
loss = criterion(output, y)
# 梯度清零
optimizer.zero_grad()
# 反向传播
loss.backward()
# 参数更新
optimizer.step()
# 损失计算
total_num += len(y)
total_loss += loss.item() * len(y)
# 打印损失变换结果
print('epoch: %4s loss: %.2f, time: %.2fs' %
(epoch_idx + 1, total_loss / total_num, time.time() - start))
# 模型保存
torch.save(model.state_dict(), './model/phone-price-model2.pth')


def test(valid_dataset, input_dim, class_num):
# 加载模型和训练好的网络参数
model = PhonePriceModel(input_dim, class_num)
# load_state_dict:将加载的参数字典应用到模型上
# load:加载用来保存模型参数的文件
model.load_state_dict(torch.load('./model/phone-price-model2.pth'))
# 构建加载器
dataloader = DataLoader(valid_dataset, batch_size=8, shuffle=False)
# 评估测试集
correct = 0
# 遍历测试集中的数据
for x, y in dataloader:
# 将其送入网络中
# model.eval()
output = model(x)
# 获取预测类别结果
y_pred = torch.argmax(output, dim=1)
# 获取预测正确的个数
correct += (y_pred == y).sum()
# 求预测精度
print('Acc: %.5f' % (correct / len(valid_dataset)))


if __name__ == '__main__':
train_dataset, valid_dataset, input_dim, class_num = create_dataset()
train(train_dataset, input_dim, class_num)
test(valid_dataset, input_dim, class_num)

Python编程_推导式与函数

今日内容大纲

  • Python推导式定义与应用

  • 函数定义与嵌套 ※※

  • 变量的作用域

  • 函数参数进阶 ※※

  • lambda函数应用场景

推导式

学习目标

  • 掌握推导式的相关用法

什么是推导式

推导式comprehensions(又称解析式),是Python的一种独有特性。推导式是可以从一个数据序列构建另一个新的数据序列(一个有规律的列表或控制一个有规律列表)的结构体。 共有三种推导:列表推导式集合推导式字典推导式

为什么需要推导式

案例:创建一个0-9的列表

while循环:

1
2
3
4
5
6
7
8
9
# 初始化计数器
i = 0
list1 = []
# 编写循环条件
while i <= 9:
list1.append(i)
# 更新计数器
i += 1
print(list1)

for循环:

1
2
3
4
5
list1 = []
# 编写for循环
for i in range(0, 10):
list1.append(i)
print(list1)

思考:我们能不能把以上代码简化为一行代码搞定这个程序呢?

答:可以,使用推导式

列表推导式

基本语法:

1
2
3
变量名 = [表达式 for 变量 in 列表]
变量名 = [表达式 for 变量 in 列表 if 条件]
变量名 = [表达式 for 变量 in 列表 for 变量 in 列表]

案例:定义0-9之间的列表

1
2
3
4
list1 = []
for i in range(10):
list1.append(i)
print(list1)

列表推导式

1
2
list1 = [i for i in range(10)]
print(list1)

执行原理:[i for i in range(10)]

1
2
3
4
5
6
列表推导式先运行表达式右边的内容:

当第一次遍历时:i = 0,其得到变量i的结果后,会放入最左侧的变量i中,这个时候列表中就是[0]
当第二次遍历时:i = 1,其得到变量i的结果后,会追加最左侧的变量i中,这个时候列表中就是[0, 1]
...
当最后一次遍历时:i = 9,其得到变量i的结果后,会追加最左侧的变量i中,这个时候列表中就是[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

列表推导式 + if条件判断

在使用列表推导式时候,我们除了可以使用for循环,其实我们还可以在其遍历的过程中,引入if条件判断。

1
2
3
4
5
6
变量 = [表达式 for 临时变量 in 序列 if 条件判断]

等价于

for 临时变量 in 序列:
if 条件判断

案例:生成0-9之间的偶数(i%2 == 0)序列

1
2
list1 = [i for i in range(10) if i % 2 == 0]
print(list1)

for循环嵌套列表推导式

1
2
for 临时变量 in range(n):
for 临时变量 in range(n):

基本语法:

1
变量 = [表达式 for 临时变量 in 序列 for 临时变量 in 序列]

案例:创建列表 => [(1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]

(1,0) (1, 0-2)

(1,1) (1, 0-2)

(1,2) (1, 0-2)


(2,0) (2, 0-2)

(2,1) (2, 0-2)

(2,2) (2, 0-2)

原生代码:for循环嵌套

1
2
3
4
5
6
7
8
list1 = []
# 外层循环
for i in range(1, 3):
# 内层循环
for j in range(0, 3):
tuple1 = (i, j)
list1.append(tuple1)
print(list1)

列表推导式:

1
2
list1 = [(i, j) for i in range(1, 3) for j in range(0, 3)]
print(list1)

推导式案例

案例一:使用列表推导式生成平方数集合

1
2
3
4
5
6
7
8
9
10
11
# 定义要生成的平方数的数量
n = 10

# 使用列表推导式生成平方数列表
squares_list = [i**2 for i in range(1, n + 1)]

# 将列表转换为集合
squares_set = set(squares_list)

# 输出结果
print("平方数集合:", squares_set)

巩固练习

使用字段表达式将列表中元素映射为键值对,形成字典

1
2
3
4
5
6
7
8
9
10
11
12
13
# 定义名字列表
names = ['Alice', 'Bob', 'Charlie']

# 定义年龄列表
ages = [25, 30, 35]

# 使用字典推导式将两个列表拼接成字典
people_dict = {names[i]: ages[i] for i in range(len(names))}

# 输出结果
print("\n名字列表:", names)
print("年龄列表:", ages)
print("拼接后的字典:", people_dict)

Python中函数的作用与使用步骤

学习目标

  • 掌握函数的定义和调用格式

为什么需要函数

在Python实际开发中,我们使用函数的目的只有一个“让我们的代码可以被重复使用”

函数的作用有两个:

==① 代码重用(代码重复使用)==

==② 模块化编程(模块化编程的核心就是函数,一般是把一个系统分解为若干个功能,每个功能就是一个函数)==

在编程领域,编程可以分为两大类:① 模块化编程 ② 面向对象编程

什么是函数

所谓的函数就是一个==被命名的==、==独立的、完成特定功能的代码段(一段连续的代码)==,并可能给调用它的程序一个==返回值==。

被命名的:在Python中,函数大多数是有名子的函数(普通函数)。当然Python中也存在没有名字的函数叫做匿名函数。

独立的、完成特定功能的代码段:在实际项目开发中,定义函数前一定要先思考一下,这个函数是为了完成某个操作或某个功能而定义的。(函数的功能一定要专一)

返回值:很多函数在执行完毕后,会通过return关键字返回一个结果给调用它的位置。

函数的定义

基本语法:

1
2
3
4
def 函数名称([参数1, 参数2, ...]):
函数体
...
[return 返回值]

函数的调用

在Python中,函数和变量一样,都是先定义后使用。

1
2
3
4
5
6
7
8
# 定义函数
def 函数名称([参数1, 参数2, ...]):
函数体
...
[return 返回值]

# 调用函数
函数名称(参数1, 参数2, ...)

通过一个栗子引入函数

① 使用Python代码,编写一个打招呼程序

1
2
3
4
5
6
第一步:见到一个老师,打一声招呼
print('您好')
第二步:见到一个老师,打一声招呼
print('您好')
第二步:见到一个老师,打一声招呼
print('您好')

虽然以上程序可以满足程序的需求,但是我们发现,我们的代码做了很多重复性的工作。我们能不能对以上代码进行进一步的优化,避免代码的重复性编写。

② 升级:使用Python代码,编写一个打招呼程序(函数——一次编写,多次利用)

1
2
3
4
5
6
7
8
9
10
11
# 定义函数(封装函数)
def greet():
print('您好')

# 调用函数
# 见到一个老师,打一声招呼
greet()
# 见到一个老师,打一声招呼
greet()
# 见到一个老师,打一声招呼
greet()

③ 升级:使用Python代码编写一个打招呼程序,可以实现向不同的人打不同的招呼

1
2
3
4
5
6
7
8
9
10
11
# 定义一个函数,同时为其定义一个参数
def greet(name):
print(f'{name},您好')

# 调用函数
# 见到了张老师,打一声招呼
greet('老张')
# 见到了李老师,打一声招呼
greet('老李')
# 见到了王老师,打一声招呼
greet('老王')

④ 函数的设计原则“高内聚、低耦合”,函数执行完毕后,应该主动把数返回给调用处,而不应该都交由print()等函数直接输出。

1
2
3
4
5
6
7
8
9
10
11
12
# 定义一个函数,拥有name参数,同时函数执行完毕后,拥有一个return返回值
def greet(name):
# 执行一系列相关操作
return name + ',您好'

# 调用函数
# 见到了张老师,打一声招呼
print(greet('老张'))
# 见到了李老师,打一声招呼
print(greet('老李'))
# 见到了王老师,打一声招呼
print(greet('老王'))

聊聊return返回值

思考1:如果一个函数如些两个return (如下所示),程序如何执行?

1
2
3
4
5
6
7
def return_num():
return 1
return 2


result = return_num()
print(result) # 1

答:只执行了第一个return,原因是因为return可以退出当前函数,导致return下方的代码不执行。

思考2:如果一个函数要有多个返回值,该如何书写代码?

答:在Python中,理论上一个函数只能返回一个结果。但是如果我们向让一个函数可以同时返回多个结果,我们可以使用return 元组的形式。

1
2
3
4
5
6
7
def return_num():
return 1, 2


result = return_num()
print(result)
print(type(result)) # <class 'tuple'>

思考3:封装一个函数,参数有两个num1,num2,求两个数的四则运算结果

四则运算:加、减、乘、除

1
2
3
4
5
6
7
8
9
10
def size(num1, num2):
jia = num1 + num2
jian = num1 - num2
cheng = num1 * num2
chu = num1 / num2
return jia, jian, cheng, chu


# 调用size方法
print(size(20, 5))

什么是说明文档

思考:定义一个函数后,程序员如何书写程序能够快速提示这个函数的作用?

答:注释

思考:如果代码多,我们是不是需要在很多代码中找到这个函数定义的位置才能看到注释?如果想更方便的查看函数的作用怎么办?

答:==函数的说明文档(函数的说明文档也叫函数的文档说明)==

总结

Q1:知道函数的作用

  • 模块化编程
  • 提高代码的复用性

Q2: 函数的定义格式

def 函数名(参1, 参2…):

​ 函数体

​ return 具体的返回值

Q3: 函数的调用格式

变量名 = 函数名(实参1, 实参2…)

函数的嵌套及案例

学习目标

  • 掌握函数嵌套的格式

什么是函数的嵌套

所谓函数嵌套调用指的是==一个函数里面又调用了另外一个函数==。

函数嵌套的基本语法

image-20210313120335429

嵌套函数的执行流程:

第一步:Python代码遵循一个“顺序原则”,从上往下,从左往右一行一行执行

当代码执行到第1行时,则在计算机内存中定义一个funcB函数。但是其内部的代码并没有真正的执行,跳过第2行继续向下运行。

第二步:执行到第5行,发现又声明了一个funcA的函数,根据函数的定义原则,定义就是在内存中声明有这样一个函数,但是没有真正的调用和执行。

第三步:代码继续向下执行,到第14行,发现funcA(),函数体()就代表调用funcA函数并执行其内部的代码。程序返回到第6行,然后一步一步向下执行,输出40个横杠,然后打印这是funcA函数的函数体部分…,然后继续向下执行,遇到funcB函数,后边有一个圆括号代表执行funcB函数,原程序处于等待状态。

第四步:进入funcB函数,执行输出这是funcB函数的函数体部分…,当代码完毕后,返回funcA函数中funcB()的位置,继续向下执行,打印40个横杠。

最终程序就执行结束了。

函数的应用案例

案例:奇偶求和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
"""
需求: 奇偶求和.
编写一个程序,求出一个列表中偶数和奇数的和。
定义函数calculate_sum(),参数为一个数字列表numbers_list, 分别求出偶数和奇数的和。
最后,返回一个列表,第一个元素为偶数的和,第二个元素为奇数的和。

"""

# 1. 定义函数 calculate_sum(),参数为一个数字列表 numbers_list
def calculate_sum(numbers_list):
# 2. 初始化偶数和为0
even_sum = 0

# 3. 初始化奇数和为0
odd_sum = 0

# 4. 遍历数字列表中的每个数字
for num in numbers_list:
# 5. 检查数字是否为偶数
if num % 2 == 0:
# 6. 如果是偶数,加到偶数和中
even_sum += num
else:
# 7. 如果是奇数,加到奇数和中
odd_sum += num

# 8. 返回一个列表,第一个元素为偶数的和,第二个元素为奇数的和
return [even_sum, odd_sum]


# 9. 从输入获取一个整数列表
numbers = [2, 5, 3, 7, 5, 7]

# 10. 调用 calculate_sum() 函数并获取结果
result = calculate_sum(numbers)

# 11. 打印结果
print(f"偶数的和是: {result[0]}, 奇数的和是: {result[1]}")

总结

Q1: 函数嵌套调用流程

  • 默认按照顺序结构, 从上往下逐级调用

变量的作用域

学习目标

  • 掌握变量的作用域
  • 掌握global关键字的用法

什么是变量的作用域

变量作用域指的是变量的作用范围(变量在哪里可用,在哪里不可用),主要分为两类:全局作用域与局部作用域。

其实作用域的划分比较简单,在函数内部定义范围就称之为局部作用域,在函数外部(全局)定义范围就是全局作用域

1
2
3
# 全局作用域
def func():
# 局部作用域

局部变量与全局变量

在Python中,定义在函数外部的变量就称之为全局变量;定义在函数内部变量就称之为局部变量。

1
2
3
4
5
6
7
# 定义在函数外部的变量(全局变量)
num = 10
# 定义一个函数
def func():
# 函数体代码
# 定义在函数内部的变量(局部变量)
num = 100

变量作用域的作用范围

全局变量:在整个程序范围内都可以直接使用

1
2
3
4
5
6
7
8
9
10
str1 = 'hello'
# 定义一个函数
def func():
# 在函数内部调用全局变量str1
print(f'在局部作用域中调用str1变量:{str1}')

# 直接调用全局变量str1
print(f'在全局作用域中调用str1变量:{str1}')
# 调用func函数
func()

局部变量:在函数的调用过程中,开始定义,函数运行过程中生效,函数执行完毕后,销毁

1
2
3
4
5
6
7
8
9
10
# 定义一个函数
def func():
# 在函数内部定义一个局部变量
num = 10
print(f'在局部作用域中调用num局部变量:{num}')

# 调用func函数
func()
# 在全局作用域中调用num局部变量
print(f'在全局作用域中调用num局部变量:{num}')

运行结果:

image-20210313145728886

普及小知识:计算机的垃圾回收机制

global关键字的应用场景

思考一个问题:我们能不能在局部作用域中对全局变量进行修改呢?

1
2
3
4
5
6
7
8
9
10
11
# 定义全局变量num = 10
num = 10
# 定义一个函数func
def func():
# 尝试在局部作用域中修改全局变量
num = 20

# 调用函数func
func()
# 尝试访问全局变量num
print(num)

最终结果:弹出10,所以由运行结果可知,在函数体内部理论上是没有办法对全局变量进行修改的,所以一定要进行修改,必须使用global关键字。

1
2
3
4
5
6
7
8
9
10
11
12
# 定义全局变量num = 10
num = 10
# 定义一个函数func
def func():
# 尝试在局部作用域中修改全局变量
global num
num = 20

# 调用函数func
func()
# 尝试访问全局变量num
print(num)

记住:global关键字只是针对不可变数据类型的变量进行修改操作(数值、字符串、布尔类型、元组类型),可变类型可以不加global关键字。

总结

Q1: 变量的作用域

  • 指的是变量的作用范围(变量在哪里可用,在哪里不可用)

Q2: global关键字的用法

  • 实现在局部位置对全局变量做修改.

函数的参数进阶

学习目标

  • 掌握函数参数之位置参数, 关键字参数的用法
  • 掌握函数参数之不定长参数的用法

函数的参数

在函数定义与调用时,我们可以根据自己的需求来实现参数的传递。在Python中,函数的参数一共有两种形式:

① 形参 ② 实参

形参:在函数定义时,所编写的参数就称之为形式参数

实参:在函数调用时,所传递的参数就称之为实际参数

1
2
3
4
5
6
def greet(name):  # name就是在函数greet定义时,所编写的参数(形参)
return name + ',您好'

# 调用函数
name = '老王'
greet(name) # 在函数调用时,所传递的参数就是实际参数

注意:虽然我们在函数传递时,喜欢使用相同的名称作为参数名称。但是两者的作用范围是不同的。name = ‘老王’,代表实参。其是一个全局变量,而greet(name)函数中的name实际是在函数定义时才声明的变量,所以其实一个局部变量。

函数的参数类型(调用)

☆ 位置参数

理论上,在函数定义时,我们可以为其定义多个参数。但是在函数调用时,我们也应该传递多个参数,正常情况,其要一一对应。

1
2
3
4
5
def user_info(name, age, address):
print(f'我的名字{name},今年{age}岁了,家里住在{address}')

# 调用函数
user_info('Tom', 23, '美国纽约')

注意事项:位置参数强调的是参数传递的位置必须一一对应,不能颠倒

☆ 关键词参数(Python特有)

函数调用,通过“键=值”形式加以指定。可以让函数更加清晰、容易使用,同时也清除了参数的顺序需求。

1
2
3
4
5
def user_info(name, age, address):
print(f'我的名字{name},今年{age}岁了,家里住在{address}')

# 调用函数(使用关键词参数)
user_info(name='Tom', age=23, address='美国纽约')

函数定义时缺省参数(参数默认值)

缺省参数也叫默认参数,用于定义函数,为参数提供默认值,调用函数时可不传该默认参数的值(注意:所有位置参数必须出现在默认参数前,包括函数定义和调用)。

1
2
3
4
5
6
7
def user_info(name, age, gender='男'):
print(f'我的名字{name},今年{age}岁了,我的性别为{gender}')


user_info('李林', 25)
user_info('振华', 28)
user_info('婉儿', 18, '女')

谨记:我们在定义缺省参数时,一定要把其写在参数列表的最后侧

不定长参数

不定长参数也叫可变参数。用于不确定调用的时候会传递多少个参数(不传参也可以)的场景。此时,可用==包裹(packing)位置参数==,或者==包裹关键字参数==,来进行参数传递,会显得非常方便。

☆ 不定长元组(位置)参数

1
2
3
4
5
6
def user_info(*args):
# print(args) # 元组类型数据,对传递参数有顺序要求
print(f'我的名字{args[0]},今年{args[1]}岁了,住在{args[2]}')

# 调用函数,传递参数
user_info('Tom', 23, '美国纽约')

☆ 不定长字典(关键字)参数

1
2
3
4
5
6
def user_info(**kwargs):
# print(kwargs) # 字典类型数据,对传递参数没有顺序要求,格式要求key = value值
print(f'我的名字{kwargs["name"]},今年{kwargs["age"]}岁了,住在{kwargs["address"]}')

# 调用函数,传递参数
user_info(name='Tom', address='美国纽约', age=23)

kw = keyword + args

综上:无论是包裹位置传递还是包裹关键字传递,都是一个组包的过程。

Python组包:就是把多个数据组成元组或者字典的过程。

案例:Python中数据的传递案例

1
2
3
4
5
6
7
8
9
10
11
12
13
def func(*args, **kwargs):
print(args)
print(kwargs)


# 定义一个元组(也可以是列表)
tuple1 = (10, 20, 30)
# 定义一个字典
dict1 = {'first': 40, 'second': 50, 'third': 60}
# 需求:把元组传递给*args参数,字典传递给**kwargs
# ① 如果想把元组传递给*args,必须在tuple1的前面加一个*号
# ② 如果想把字典传递给**kwargs,必须在dict1的前面加两个**号
func(*tuple1, **dict1)

可变与非可变数据类型

引用变量

在大多数编程语言中,值的传递通常可以分为两种形式“值传递与引用(地址)传递”,但是在Python中变量的传递基本上都是引用(地址)传递。

☆ 聊聊变量在内存底层的存储形式

1
a = 10

第一步:首先在计算机内存中创建一个数值10(占用一块内存空间)

第二步:在栈空间中声明一个变量,如a

第三步:把数值10的内存地址赋予给变量小a,形成所谓的==“引用关系”==

image-20210315100015772

☆ 如何验证Python中变量的引用关系

答:可以使用内置方法id(),其参数就是要显示的变量信息 => id(变量名称)

1
2
a = 10
print(id(a))

☆ 把一个变量赋予给另外一个变量的影响

1
2
3
4
a = 10
b = a
print(id(a))
print(id(b))

运行结果:

image-20210315100748118

说明:由以上运行结果可知,当我们把一个变量赋予给另外一个变量时,其两者指向的内存地址相同。就说明a和b指向了同一块内存空间,原理图如下:

image-20210315101115722

思考:如果在b = a以后,我们改变了变量a的值,问变量b是否会受到影响?

1
2
3
4
5
6
7
8
9
10
11
# a = 10
# print(id(a))

a = 10
b = a

a = 100
print(b) # 10 或 100

print(id(a))
print(id(b))

原理图:

image-20210315101547645

总结:不可变数据类型(数值)在赋值以后,其中一个值的改变不影响另外一个变量,因为两者指向空间地址不同。

Python中可变和非可变数据类型

☆ 问题1:在Python中一共有几种数据类型?

答:7种,数值(int整型、float浮点类型)、bool类型(True和False)、字符串类型(str)、元组(tuple 1,2,3)、列表(list [1, 2, 3])、字典(dict {key:value})、集合(set {1, 2})

在Python中,我们可以把7种数据类型分为两大类:可变类型 + 非可变类型

① 不可变类型(内存地址一旦固定,其值就不能发生改变)

数值(int整型、float浮点类型)

bool类型(True和False)

字符串类型(str)

元组(tuple 1,2,3)

② 可变类型(内存地址一旦固定,其值是可以发生改变)

列表(list [1, 2, 3])

字典(dict {key:value})

集合(set {1, 2})

☆ 问题2:如何判断一个数据类型是可变类型还是非可变类型?

在Python中,可变类型与非可变类型主要是通过这个数据类型在内存中的表现形式来进行定义的。

==① 可变类型就是在内存中,其内存地址一旦固定,其变量的值是可以发生改变的==

1
2
3
4
5
6
a = [1, 2, 3]
print(id(a))

# 向内存中追加新数据(对数据进行改变只能通过数据类型.方法()实现)
a.append(4)
print(id(a))

原理图:

image-20210315103357217

==② 非可变类型就是在内存中,内存地址一旦固定,其变量的值就没办法发生任何改变了==

1
2
3
4
5
a = 10
print(id(a))

a = 'hello'
print(id(a))

原理图:

image-20210315103823240

可变类型与非可变类型在函数中的应用

☆ 可变类型

1
2
3
4
5
6
7
8
9
10
# 定义一个函数
def func(mynames):
mynames.append('赵六')

# 定义一个全局变量
names = ['张三', '李四', '王五']
# 调用函数
func(names)

print(names) # ???

原理图:

image-20210315110548353

综上所述:可变类型在函数中,如果在全局或局部中对可变类型进行增删改操作,其外部和内部都会受到影响。

☆ 不可变类型

1
2
3
4
5
6
7
8
9
10
11
# 定义一个函数
def func(num):
num += 1
print(num)

# 定义一个全局变量
a = 10
# 调用函数
func(a)
# 在全局作用域中打印a
print(a)

image-20210315111142925

综上所述:不可变类型在函数中,局部或全局的改变对外部和内部都没有任何影响。

元组拆包

什么是拆包?

简单来说就是把一个元组中的数据一个一个拆解出来的过程,就称之为叫做拆包操作。

基本语法

1
2
3
4
5
6
7
8
9
tuple1 = (10, 20)
# 拆包
num1, num2 = tuple1

以上代码可以简写为
num1, num2 = (10, 20)

还可以进一步简写
num1, num2 = 10, 20

拆包典型案例

实现两个变量值的交换

① 引入第三方变量实现数值交换

1
2
3
4
5
6
7
8
9
10
c1 = '可乐'
c2 = '牛奶'

# 经过一系列Python操作,把c1和c2中的值进行交换
temp = c1
c1 = c2
c2 = temp

print(c1) # 牛奶
print(c2) # 可乐

② 使用元组拆包实现两个变量值交换

1
2
3
4
5
6
7
c1 = '可乐'
c2 = '牛奶'

c1, c2 = (c2, c1)

print(c1) # 牛奶
print(c2) # 可乐

lambda函数

普通函数与匿名函数

在Python中,函数是一个被命名的、独立的完成特定功能的一段代码,并可能给调用它的程序一个返回值。

所以在Python中,函数大多数是有名函数 => 普通函数。但是有些情况下,我们为了简化程序代码,也可以定义匿名函数 => lambda表达式

lambda表达式应用场景

如果一个函数有一个返回值,并且只有一句代码,可以使用 lambda简化。

lambda表达式基本语法

1
2
3
变量 = lambda 函数参数:表达式(函数代码 + return返回值)
# 调用变量
变量()

编写lambda表达式

定义一个函数,经过一系列操作,最终返回100

1
2
3
4
5
6
def fn1():
return 100

# 调用fn1函数
print(fn1) # 返回fn1函数在内存中的地址
print(fn1()) # 代表找到fn1函数的地址并立即执行

lambda表达式进行简化:

1
2
3
4
fn2 = lambda : 100

print(fn2) # 返回fn2在内存中的地址
print(fn2())

编写带参数的lambda表达式

编写一个函数求两个数的和

1
2
3
4
def fn1(num1, num2):
return num1 + num2

print(fn1(10, 20))

lambda表达式进行简化:

1
2
3
fn2 = lambda num1, num2:num1 + num2

print(fn2(10, 20))

☆ 带默认参数的lambda表达式

1
2
fn = lambda a, b, c=100 : a + b + c
print(fn(10, 20))

☆ 带if判断(三目运算符)的lambda表达式

1
2
3
fn = lambda a, b : a if a > b else b

print(fn(10, 20))

☆ 列表数据+字典数据排序(重点)

知识点:列表.sort(key=排序的key索引, reverse=True)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
students = [
{'name': 'Tom', 'age': 20},
{'name': 'Rose', 'age': 19},
{'name': 'Jack', 'age': 22}
]

# 按name值升序排列
students.sort(key=lambda x: x['name']) # Jack/Rose/Tom
print(students)

# 按name值降序排列
students.sort(key=lambda x: x['name'], reverse=True)
print(students)

# 按age值升序排列
students.sort(key=lambda x: x['age'])
print(students)

作业

作业一_基础题:模拟计算器

需求:使用函数方式定义一个计算器,要求函数可以接收两个整数,并且可以返回这两个整数的加减乘除结果

作业二_进阶题:最近回文数

需求:查找最近回文数.回文数是指从前往后读和从后往前读都是一样的数字,比如 121。最近的回文数指的是离给定数字最近的回文数,比如给定数字 125,最近的回文数是121。编写一个程序,查找离给定整数最近的回文数。

逻辑回归

逻辑回归

逻辑回归简介

学习目标:

1.知道逻辑回归的应用场景

2.复习逻辑回归应用到的数学知识

【了解】应用场景

image-20230904115156371

逻辑回归是解决二分类问题的利器

【熟悉】数学知识

【知道】sigmoid函数

image-20230913152535607

image-20230904144434180

image-20230904144454316

【理解】概率

image-20230904144658969

【理解】极大似然估计

核心思想:

设模型中含有待估参数w,可以取很多值。已经知道了样本观测值,从w的一切可能值中(选出一个使该观察值出现的概率为最大的值,作为w参数的估计值,这就是极大似然估计。(顾名思义:就是看上去那个是最大可能的意思)

举个例子:

假设有一枚不均匀的硬币,出现正面的概率和反面的概率是不同的。假定出现正面的概率为𝜃, 抛了6次得到如下现象 D = {正面,反面,反面,正面,正面,正面}。每次投掷事件都是相互独立的。 则根据产生的现象D,来估计参数𝜃是多少?

1
2
3
4
P(D|𝜃) = P {正面,反面,反面,正面,正面,正面}
= P(正面|𝜃) P(正面|𝜃) P(正面|𝜃) P(正面|𝜃) P(正面|𝜃) P(正面|𝜃)

=𝜃 *(1-𝜃)*(1-𝜃)𝜃*𝜃*𝜃 = 𝜃4(1 − 𝜃)

问题转化为:求此函数的极大值时,估计𝜃为多少

image-20230904145453737

【知道】对数函数

image-20230904145530932

逻辑回归原理

学习目标
1.理解逻辑回归算法的原理

2.知道逻辑回归的损失函数

【理解】原理

逻辑回归概念 Logistic Regression

• 一种分类模型,把线性回归的输出,作为逻辑回归的输入。

• 输出是(0, 1)之间的值

• 基本思想

  1. 利用线性模型 f(x) = wx + b 根据特征的重要性计算出一个值
  2. 再使用 sigmoid 函数将 f(x) 的输出值映射为概率值
    1. 设置阈值(eg:0.5),输出概率值大于 0.5,则将未知样本输出为 1 类
    2. 否则输出为 0 类

3.逻辑回归的假设函数
h(w) = sigmoid(wx + b )

线性回归的输出,作为逻辑回归的输入

在逻辑回归中,当预测结果不对的时候,我们该怎么衡量其损失呢?

我们来看下图(下图中,设置阈值为0.6),

image-20220121161828121

那么如何去衡量逻辑回归的预测结果与真实结果的差异?

【知道】损失函数

image-20230904151300483

image-20230904151334262

image-20230904151343779

逻辑回归API

学习目标:

1.知道逻辑回归的API

2.动手实现癌症分类案例

【知道】API介绍

1
sklearn.linear_model.LogisticRegression(solver='liblinear', penalty=‘l2’, C = 1.0)

solver 损失函数优化方法:

训练速度:liblinear 对小数据集场景训练速度更快,sag 和 saga 对大数据集更快一些。 2 正则化:

  1. newton-cg、lbfgs、sag、saga 支持 L2 正则化或者没有正则化
  2. 2liblinear 和 saga 支持 L1 正则化

penalty:正则化的种类,l1 或者 l2

C:正则化力度

默认将类别数量少的当做正例

【实践】癌症分类案例

  • 数据介绍

(1)699条样本,共11列数据,第一列用语检索的id,后9列分别是与肿瘤相关的医学特征,

​ 最后一列表示肿瘤类型的数值。

(2)包含16个缺失值,用”?”标出。

(3)2表示良性,4表示恶性

数据描述

(1)699条样本,共11列数据,第一列用语检索的id,后9列分别是与肿瘤

相关的医学特征,最后一列表示肿瘤类型的数值。

(2)包含16个缺失值,用”?”标出。

  • 案例分析
1
2
3
4
5
6
7
8
1.获取数据
2.基本数据处理
2.1 缺失值处理
2.2 确定特征值,目标值
2.3 分割数据
3.特征工程(标准化)
4.机器学习(逻辑回归)
5.模型评估
  • 代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler


def dm01_LogisticRegression():
# 1. 获取数据.
data = pd.read_csv('data/breast-cancer-wisconsin.csv')
data.info()

# 2. 数据预处理.
# data = data.replace(to_replace='?', value=np.NAN)
data = data.replace('?', np.NaN)
data = data.dropna()
data.info()

# 3. 确定特征值和目标值.
x = data.iloc[:, 1:-1]
y = data.Class
print(f'x.head(): {x.head()}')
print(f'y.head(): {y.head()}')

# 3. 分割数据.
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=21)
# 4. 特征处理.
transfer = StandardScaler()
x_train = transfer.fit_transform(x_train)
x_test = transfer.transform(x_test)
# 5. 模型训练.
estimator = LogisticRegression()
estimator.fit(x_train, y_train)
# 6. 模型预测
y_predict = estimator.predict(x_test)
print(f'预测值: {y_predict}')

# 7. 模型评估
print(f'准确率: {estimator.score(x_test, y_test)}')
print(f'准确率: {accuracy_score(y_test, y_predict)}')

if __name__ == '__main__':
dm01_LogisticRegression()

分类评估方法

学习目标:

1.理解混淆矩阵的构建方法

2.掌握精确率,召回率和F1score的计算方法

3.知道ROC曲线和AUC指标

【理解】混淆矩阵

image-20230904162952115

混淆矩阵作用在测试集样本集中:

  1. 真实值是 正例 的样本中,被分类为 正例 的样本数量有多少,这部分样本叫做真正例(TP,True Positive)
  2. 真实值是 正例 的样本中,被分类为 假例 的样本数量有多少,这部分样本叫做伪反例(FN,False Negative)
  3. 真实值是 假例 的样本中,被分类为 正例 的样本数量有多少,这部分样本叫做伪正例(FP,False Positive)
  4. 真实值是 假例 的样本中,被分类为 假例 的样本数量有多少,这部分样本叫做真反例(TN,True Negative)

True Positive :表示样本真实的类别
Positive :表示样本被预测为的类别

例子:

样本集中有 6 个恶性肿瘤样本,4 个良性肿瘤样本,我们假设恶性肿瘤为正例,则:

模型 A: 预测对了 3 个恶性肿瘤样本,4 个良性肿瘤样本

  1. 真正例 TP 为:3
  2. 伪反例 FN 为:3
  3. 伪正例 FP 为:0
  4. 真反例 TN:4

模型 B: 预测对了 6 个恶性肿瘤样本,1个良性肿瘤样本

  1. 真正例 TP 为:6
  2. 伪反例 FN 为:0
  3. 伪正例 FP 为:3
  4. 真反例 TN:1

我们会发现:TP+FN+FP+TN = 总样本数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
import pandas as pd


def dm01_混淆矩阵四个指标():
# 1. 定义样本集, 6个恶性肿瘤样本, 4个良性肿瘤样本
y_true = ['恶性', '恶性', '恶性', '恶性', '恶性', '恶性', '良性', '良性', '良性', '良性']

# 2. 定义标签
labels = ['恶性', '良性']

# 3. 模型A => 预测对了 3个恶性, 4个良性.
y_pred_A = ['恶性', '恶性', '恶性', '良性', '良性', '良性', '良性', '良性', '良性', '良性']
result = confusion_matrix(y_true, y_pred_A)
print(pd.DataFrame(result, index=labels, columns=labels))

# 4. 模型B => 预测对了6个恶性肿瘤样本, 1个良性肿瘤样本.
y_pred_B = ['恶性', '恶性', '恶性', '恶性', '恶性', '恶性', '恶性', '恶性', '恶性', '良性']
result = confusion_matrix(y_true, y_pred_B)
print(pd.DataFrame(result, index=labels, columns=labels))


# 5. 计算精确率 (precision)
print(f'模型A精确率: {precision_score(y_true, y_pred_A, pos_label="恶性")}')
print(f'模型B精确率: {precision_score(y_true, y_pred_B, pos_label="恶性")}')

# 6. 计算召回率 (recall)
print(f'模型A召回率: {recall_score(y_true, y_pred_A, pos_label="恶性")}')
print(f'模型B召回率: {recall_score(y_true, y_pred_B, pos_label="恶性")}')

# 7. 计算F1值
print(f'模型A F1值: {f1_score(y_true, y_pred_A, pos_label="恶性")}')
print(f'模型B F1值: {f1_score(y_true, y_pred_B, pos_label="恶性")}')


if __name__ == '__main__':
dm01_混淆矩阵四个指标()

【掌握】Precision(精确率)

精确率也叫做查准率,指的是对正例样本的预测准确率。比如:我们把恶性肿瘤当做正例样本,则我们就需要知道模型对恶性肿瘤的预测准确率。

image-20210709223432330

例子:

样本集中有 6 个恶性肿瘤样本,4 个良性肿瘤样本,我们假设恶性肿瘤为正例,则:

模型 A: 预测对了 3 个恶性肿瘤样本,4 个良性肿瘤样本

  1. 真正例 TP 为:3
  2. 伪反例 FN 为:3
  3. 假正例 FP 为:0
  4. 真反例 TN:4
  5. 精准率:3/(3+0) = 100%

模型 B: 预测对了 6 个恶性肿瘤样本,1个良性肿瘤样本

  1. 真正例 TP 为:6
  2. 伪反例 FN 为:0
  3. 假正例 FP 为:3
  4. 真反例 TN:1
  5. 精准率:6/(6+3) = 67%

【掌握】Recall(召回率)

召回率也叫做查全率,指的是预测为真正例样本占所有真实正例样本的比重。例如:我们把恶性肿瘤当做正例样本,则我们想知道模型是否能把所有的恶性肿瘤患者都预测出来。

例子:

样本集中有 6 个恶性肿瘤样本,4 个良性肿瘤样本,我们假设恶性肿瘤为正例,则:

模型 A: 预测对了 3 个恶性肿瘤样本,4 个良性肿瘤样本

  1. 真正例 TP 为:3
  2. 伪反例 FN 为:3
  3. 假正例 FP 为:0
  4. 真反例 TN:4
  5. 精准率:3/(3+0) = 100%
  6. 召回率:3/(3+3)=50%

模型 B: 预测对了 6 个恶性肿瘤样本,1个良性肿瘤样本

  1. 真正例 TP 为:6
  2. 伪反例 FN 为:0
  3. 假正例 FP 为:3
  4. 真反例 TN:1
  5. 精准率:6/(6+3) = 67%
  6. 召回率:6/(6+0)= 100%

【掌握】F1-score

如果我们对模型的精度、召回率都有要求,希望知道模型在这两个评估方向的综合预测能力如何?则可以使用 F1-score 指标。

样本集中有 6 个恶性肿瘤样本,4 个良性肿瘤样本,我们假设恶性肿瘤为正例,则:

模型 A: 预测对了 3 个恶性肿瘤样本,4 个良性肿瘤样本

  1. 真正例 TP 为:3
  2. 伪反例 FN 为:3
  3. 假正例 FP 为:0
  4. 真反例 TN:4
  5. 精准率:3/(3+0) = 100%
  6. 召回率:3/(3+3)=50%
  7. F1-score:(2*3)/(2*3+3+0)=67%

模型 B: 预测对了 6 个恶性肿瘤样本,1个良性肿瘤样本

  1. 真正例 TP 为:6
  2. 伪反例 FN 为:0
  3. 假正例 FP 为:3
  4. 真反例 TN:1
  5. 精准率:6/(6+3) = 67%
  6. 召回率:6/(6+0)= 100%
  7. F1-score:(2*6)/(2*6+0+3)=80%

【知道】ROC曲线和AUC指标

ROC 曲线

ROC 曲线:我们分别考虑正负样本的情况:

  1. 正样本中被预测为正样本的概率,即:TPR (True Positive Rate)
  2. 负样本中被预测为正样本的概率,即:FPR (False Positive Rate)

image-20230904182146483

ROC 曲线图像中,4 个特殊点的含义:

  1. (0, 0) 表示所有的正样本都预测为错误,所有的负样本都预测正确
  2. (1, 0) 表示所有的正样本都预测错误,所有的负样本都预测错误
  3. (1, 1) 表示所有的正样本都预测正确,所有的负样本都预测错误
  4. (0, 1) 表示所有的正样本都预测正确,所有的负样本都预测正确

绘制 ROC 曲线

假设:在网页某个位置有一个广告图片或者文字,该广告共被展示了 6 次,有 2 次被浏览者点击了。每次点击的概率如下:

样本 是否被点击 预测点击概率
1 1 0.9
2 0 0.7
3 1 0.8
4 0 0.6
5 0 0.5
6 0 0.4

根据预测点击概率排序之后:

样本 是否被点击 预测点击概率
1 1 0.9
3 1 0.8
2 0 0.7
4 0 0.6
5 0 0.5
6 0 0.4

绘制 ROC 曲线:

阈值:0.9

  1. 原本为正例的 1、3 号的样本中 3 号样本被分类错误,则 TPR = 1/2 = 0.5
  2. 原本为负例的 2、4、5、6 号样本没有一个被分为正例,则 FPR = 0

阈值:0.8

  1. 原本为正例的 1、3 号样本被分类正确,则 TPR = 2/2 = 1
  2. 原本为负例的 2、4、5、6 号样本没有一个被分为正例,则 FPR = 0

阈值:0.7

  1. 原本为正例的 1、3 号样本被分类正确,则 TPR = 2/2 = 1
  2. 原本为负类的 2、4、5、6 号样本中 2 号样本被分类错误,则 FPR = 1/4 = 0.25

阈值:0.6

  1. 原本为正例的 1、3 号样本被分类正确,则 TPR = 2/2 = 1
  2. 原本为负类的 2、4、5、6 号样本中 2、4 号样本被分类错误,则 FPR = 2/4 = 0.5

阈值:0.5

  1. 原本为正例的 1、3 号样本被分类正确,则 TPR = 2/2 = 1
  2. 原本为负类的 2、4、5、6 号样本中 2、4、5 号样本被分类错误,则 FPR = 3/4 = 0.75

阈值 0.4

  1. 原本为正例的 1、3 号样本被分类正确,则 TPR = 2/2 = 1
  2. 原本为负类的 2、4、5、6 号样本全部被分类错误,则 FPR = 4/4 = 1

(0, 0.5)、(0, 1)、(0.25, 1)、(0.5, 1)、(0.75, 1)、(1, 1)

由 TPR 和 FPR 构成的 ROC 图像为:

image-20230914105309023

AUC 值

  1. 我们发现:图像越靠近 (0,1) 点则模型对正负样本的辨别能力就越强
  2. 我们发现:图像越靠近 (0, 1) 点则 ROC 曲线下面的面积就会越大
  3. AUC 是 ROC 曲线下面的面积,该值越大,则模型的辨别能力就越强
  4. AUC 范围在 [0, 1] 之间
  5. 当 AUC= 1 时,该模型被认为是完美的分类器,但是几乎不存在完美分类器

AUC 值主要评估模型对正例样本、负例样本的辨别能力.

分类评估报告api

1
2
3
4
5
6
7
8
sklearn.metrics.classification_report(y_true, y_pred, labels=[], target_names=None )
'''
y_true:真实目标值
y_pred:估计器预测目标值
labels:指定类别对应的数字
target_names:目标类别名称
return:每个类别精确率与召回率
'''

AUC计算API

1
2
3
4
5
from sklearn.metrics import roc_auc_score
sklearn.metrics.roc_auc_score(y_true, y_score)
计算ROC曲线面积,即AUC值
y_true:每个样本的真实类别,必须为0(反例),1(正例)标记
y_score:预测得分,可以是正例的估计概率、置信值或者分类器方法的返回值

【实践】电信客户流失预测

学习目标:

1.了解案例的背景信息

2.知道案例的处理流程

3.动手实现电信客户流失案例的代码

数据集介绍

  • 流失用户指的使用过产品因为某些原因不再使用该产品。随着产品的更新迭代,都会存在一定的流失情况,这时正常现象。流失用户的比例和变化趋势能够反映该产品当前是否存在问题以及未来的发展趋势。
  • 当用户群体庞大时,有限的人力和精力不能为每个用户都投入大量的时间。如果公司可以预测哪些用户可能提前流失,这样就能将主要精力聚焦于这些用户,实施有效的用户挽留策略,提高用户粘性。
  • 本项目旨在通过分析特征属性确定用户流失的原因,以及哪些因素可能导致用户流失。建立预测模型来判断用户是否流失,并提出用户流失预警策略。
  • 具体数据说明如下:数据集中总计7043条数据,21个特征字段,最终分类特征为Churn:用户是否流失,具体内容如下所示:
    customerID:用户ID
    gender:性别
    SeniorCitizen:是否是老人
    Partner:是否有伴侣
    Dependents:是否有需要抚养的孩子
    tenure:任职
    PhoneService:是否办理电话服务
    MultipleLines:是否开通了多条线路
    InternetService:是否开通网络服务和开通的服务类型(光纤、电话拨号)
    TechSupport:是否办理技术支持服务
    OnlineBackup:是否办理在线备份服务
    OnlineSecurity:是否办理在线安全服务
    DeviceProtection:是否办理设备保护服务
    StreamingTV:是否办理电视服务
    StreamingMovies:是否办理电影服务
    Contract:签订合约的时长
    PaperlessBilling:是否申请了无纸化账单
    PaymentMethod:付款方式(电子支票、邮寄支票、银行自动转账、信用卡自动转账)
    MonthlyCharges:月消费
    TotalCharges:总消费
    Churn:用户是否流失

处理流程

1、数据基本处理

	查看数据的基本信息

​ 对类别数据数据进行one-hot处理

​ 查看标签分布情况

2、特征筛选

​ 分析哪些特征对标签值影响大

​ 初步筛选出对标签影响比较大的特征,形成x、y

3、模型训练

​ 模型训练

​ 交叉验证网格搜索等

4、模型评估

​ 精确率

​ Roc_AUC指标计算

案例实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score


# 1. 定义函数, 表示: 数据基本处理
def dm01_数据基本处理():
# 1. 读取数据, 查看数据的基本信息.
churn_pd = pd.read_csv('data/churn.csv')
# churn_pd.info()
# print(f'churn_pd.describe(): {churn_pd.describe()}')
# print(f'churn_pd: {churn_pd}')

# 2. 处理类别型的数据, 类别型数据做 one-hot编码(热编码).
churn_pd = pd.get_dummies(churn_pd)
churn_pd.info()
# print(f'churn_pd: {churn_pd}')

# 3. 去除列 Churn_No, gender_Male
churn_pd.drop(['Churn_No', 'gender_Male'], axis=1, inplace=True) # 按列删除
print(f'churn_pd: {churn_pd}')

# 4. 列标签重命名, 打印列名
churn_pd.rename(columns={'Churn_Yes': 'flag'}, inplace=True)
print(f'列名: {churn_pd.columns}')

# 5. 查看标签的分布情况 0.26用户流失
value_counts = churn_pd.flag.value_counts()
print(value_counts)


# 2. 定义函数, 表示: 特征筛选
def dm02_特征筛选():
# 1. 读取数据
churn_pd = pd.read_csv('data/churn.csv')
# 2. 处理类别型的数据, 类别型数据做 one-hot编码(热编码).
churn_pd = pd.get_dummies(churn_pd)
# 3. 去除列 Churn_No, gender_Male
churn_pd.drop(['Churn_No', 'gender_Male'], axis=1, inplace=True)
# 4. 列标签重命名
churn_pd.rename(columns={'Churn_Yes': 'flag'}, inplace=True)
# 5. 查看标签的分布情况
value_counts = churn_pd.flag.value_counts()
print(value_counts)
# 6. 查看Contract_Month 是否预签约流失情况
sns.countplot(data=churn_pd, x='Contract_Month', hue='flag')
plt.show()


# 3. 定义函数, 表示: 模型训练 和 评测
def dm03_模型训练和评测():
# 1. 读取数据
churn_pd = pd.read_csv('data/churn.csv')

# 2. 数据预处理
# 2.1 处理类别型的数据, 类别型数据做 one-hot编码(热编码).
churn_pd = pd.get_dummies(churn_pd)
# 2.2 去除列 Churn_No, gender_Male
churn_pd.drop(['Churn_No', 'gender_Male'], axis=1, inplace=True)
# 2.3 列标签重命名
churn_pd.rename(columns={'Churn_Yes': 'flag'}, inplace=True)

# 3. 特征处理.
# 3.1 提取特征和标签
x = churn_pd[['Contract_Month', 'internet_other', 'PaymentElectronic']]
y = churn_pd['flag']
# 3.2 训练集和测试集的分割
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=21)

# 4. 模型训练.
estimator = LogisticRegression()
estimator.fit(x_train, y_train)

# 5. 模型预测
y_predict = estimator.predict(x_test)
print(f'预测结果: {y_predict}')

# 6. 模型评估
print(f'准确率: {accuracy_score(y_test, y_predict)}')
print(f'准确率: {estimator.score(x_test, y_test)}')

# 计算AUC值.
print(f'AUC值: {roc_auc_score(y_test, y_predict)}')



if __name__ == '__main__':
# dm01_数据基本处理()
# dm02_特征筛选()
dm03_模型训练和评测()

作业

  1. 使用思维导图总结逻辑回归的内容

  2. 理解分类评估方法并进行详细的描述

3.动手实现癌症分类和电信用户流失案例(数据处理,特征工程,CV。。。。)

Python编程_Python数据容器

今日大纲

  • 列表增删改查及其应用场景

  • 元组定义与使用

  • 字典的增删改查与应用

  • 集合的定义与增删查

【掌握】列表及其应用场景

学习目标

  • 掌握列表的定义格式及相关操作
  • 掌握列表嵌套相关应用

为什么需要列表

思考:有一个人的姓名(TOM)怎么书写存储程序?

答:变量。

思考:如果一个班级100位学生,每个人的姓名都要存储,应该如何书写程序?声明100个变量吗?

答:No,我们使用列表就可以了, 列表一次可以存储多个数据。

在Python中,我们把这种数据类型称之为列表。但是在其他的编程语言中,如Java、PHP、Go等等中其被称之为数组。

列表的定义

1
列表序列名称 = [列表中的元素1, 列表中的元素2, 列表中的元素3, ...]

案例演示:定义一个列表,用于保存苹果、香蕉以及菠萝

1
2
3
4
5
list1 = ['apple', 'banana', 'pineapple']
# list列表类型支持直接打印
print(list1)
# 打印列表的数据类型
print(type(list1)) # <class 'list'>

注意:列表可以一次存储多个数据且可以为不同的数据类型

列表的相关操作

列表的作用是一次性存储多个数据,程序员可以对这些数据进行的操作有:

==增、删、改、查==。

☆ 查操作

列表在计算机中的底层存储形式,列表和字符串一样,在计算机内存中都占用一段连续的内存地址,我们想访问列表中的每个元素,都可以通过==”索引下标”==的方式进行获取。

image-20210310170752234

如果我们想获取列表中的某个元素,非常简单,直接使用索引下标:

1
2
3
list1 = ['apple', 'banana', 'pineapple']
# 获取列表中的banana
print(list1[1])

查操作的相关方法:

编号 函数 作用
1 index() 指定数据所在位置的下标
2 count() 统计指定数据在当前列表中出现的次数
3 in 判断指定数据在某个列表序列,如果在返回True,否则返回False
4 not in 判断指定数据不在某个列表序列,如果不在返回True,否则返回False

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1、查找某个元素在列表中出现的位置(索引下标)
list1 = ['apple', 'banana', 'pineapple']
print(list1.index('apple')) # 0
# print(list1.index('peach')) # 报错

# 2、count()方法:统计元素在列表中出现的次数
list2 = ['刘备', '关羽', '张飞', '关羽', '赵云']
# 统计一下关羽这个元素在列表中出现的次数
print(list2.count('关羽'))

# 3、in方法和not in方法(黑名单系统)
list3 = ['192.168.1.15', '10.1.1.100', '172.35.46.128']
if '10.1.1.100' in list3:
print('黑名单IP,禁止访问')
else:
print('正常IP,访问站点信息')

☆ 增操作

编号 函数 作用
1 append() 增加指定数据到列表中
2 extend() 列表结尾追加数据,如果数据是一个序列,则将这个序列的数据逐一添加到列表
3 insert() 指定位置新增数据

☆ append()

append() :在列表的尾部追加元素

1
2
3
4
5
names = ['孙悟空', '唐僧', '猪八戒']
# 在列表的尾部追加一个元素"沙僧"
names.append('沙僧')
# 打印列表
print(names)

注意:列表追加数据的时候,直接在原列表里面追加了指定数据,即修改了原列表,故列表为可变类型数据。

☆ extend()方法

列表结尾追加数据,如果数据是一个序列,则将这个序列的数据逐一添加到列表

案例:

1
2
3
4
5
6
7
8
9
10
list1 = ['Tom', 'Rose', 'Jack']
# 1、使用extend方法追加元素"Jennify"
# names.extend("Jennify")
# print(names)

# 2、建议:使用extend方法两个列表进行合并
list2 = ['Hack', 'Jennify']
list1.extend(list2)

print(list1)

总结:extend方法比较适合于两个列表进行元素的合并操作

☆ insert()方法

作用:在指定的位置增加元素

1
2
3
4
names = ['薛宝钗', '林黛玉']
# 在薛宝钗和林黛玉之间,插入一个新元素"贾宝玉"
names.insert(1, '贾宝玉')
print(names)

☆ 删操作

编号 函数 作用
1 del 列表[索引] 删除列表中的某个元素
2 pop() 删除指定下标的数据(默认为最后一个),并返回该数据
3 remove() 移除列表中某个数据的第一个匹配项。

☆ del删除指定的列表元素

基本语法:

1
2
3
4
5
names = ['Tom', 'Rose', 'Jack', 'Jennify']
# 删除Rose
del names[1]
# 打印列表
print(names)

☆ pop()方法

作用:删除指定下标的元素,如果不填写下标,默认删除最后一个。其返回结果:就是删除的这个元素

1
2
3
4
5
6
names = ['貂蝉', '吕布', '董卓']
del_name = names.pop()
# 或
# del_name = names.pop(1)
print(del_name)
print(names)

☆ remove()方法

作用:删除匹配的元素

1
2
3
fruit = ['apple', 'banana', 'pineapple']
fruit.remove('banana')
print(fruit)

改操作

编号 函数 作用
1 列表[索引] = 修改后的值 修改列表中的某个元素
2 reverse() 将数据序列进行倒叙排列
3 sort() 对列表序列进行排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
list1 = ['貂蝉', '大乔', '小乔', '八戒']
# 修改列表中的元素
list1[3] = '周瑜'
print(list1)

list2 = [1, 2, 3, 4, 5, 6]
list2.reverse()
print(list2)

list3 = [10, 50, 20, 30, 1]
list3.sort() # 升序(从小到大)
# 或
# list3.sort(reverse=True) # 降序(从大到小)
print(list3)

列表的循环遍历

什么是循环遍历?答:循环遍历就是使用while或for循环对列表中的每个数据进行打印输出

while循环:

1
2
3
4
5
6
7
8
9
list1 = ['貂蝉', '大乔', '小乔']

# 定义计数器
i = 0
# 编写循环条件
while i < len(list1):
print(list1[i])
# 更新计数器
i += 1

for循环(个人比较推荐):

1
2
3
list1 = ['貂蝉', '大乔', '小乔']
for i in list1:
print(i)

列表的嵌套

列表的嵌套:列表中又有一个列表,我们把这种情况就称之为列表嵌套

在其他编程语言中,称之为叫做二维数组或多维数组

应用场景:要存储班级一、二、三 => 三个班级学生姓名,且每个班级的学生姓名在一个列表。

1
2
3
4
5
6
7
8
9
10
11
12
classes = ['第一个班级','第二个班级','第三个班级']

一班:['张三', '李四']
二班:['王五', '赵六']
三班:['田七', '钱八']

把班级和学员信息合并在一起,组成一个嵌套列表
students = [['张三', '李四'],['王五', '赵六'],['田七', '钱八']]

students = [x,y,z]
students[0] == ['张三', '李四']
students[0][1]

问题:嵌套后的列表,我们应该如何访问呢?

1
2
3
4
5
# 访问李四
print(students[0][1])
# 嵌套列表进行遍历,获取每个班级的学员信息
for i in students:
print(i)

列表案例

案例需求

编写一个程序,求一个正整数数组中每对相邻元素的最大值。
例如, 输入: [7 8 9 5 6 7 2 3] 输出: [8, 9, 9, 6, 7, 7, 3]

实现思路

①定义一个正整数数组

②初始化一个空列表用于存储相邻元素的最大值

③使用 for 循环遍历数组中的每对相邻元素

④获取当前元素和下一个元素

⑤计算当前元素和下一个元素的最大值

⑥将最大值添加到 max_values 列表中

⑦打印相邻元素的最大值列表

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 定义一个正整数数组
numbers = [7, 8, 9, 5, 6, 7, 2, 3]

# 2. 初始化一个空列表用于存储相邻元素的最大值
max_values = []

# 3. 使用 for 循环遍历数组中的每对相邻元素
for i in range(len(numbers) - 1):
# 4. 获取当前元素和下一个元素
current = numbers[i]
next_element = numbers[i + 1]

# 5. 计算当前元素和下一个元素的最大值
max_value = max(current, next_element)

# 6. 将最大值添加到 max_values 列表中
max_values.append(max_value)

# 7. 打印相邻元素的最大值列表
print(f"相邻元素的最大值是: {max_values}")

巩固练习

给定列表original_list其中包含1, 2, 2, 3, 4, 4, 5, 6, 6, 7元素,现在通过编写程序对列表中的数据进行去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.原始列表
original_list = [1, 2, 2, 3, 4, 4, 5, 6, 6, 7]

# 2.用于存储去重后的列表
unique_list = []

# 3.遍历原始列表
for item in original_list:
# 4.如果元素不在去重后的列表中,则添加
if item not in unique_list:
unique_list.append(item)

# 5.输出去重后的列表
print("去重后的列表:", unique_list)

总结

Q1: 什么是列表?

  • Python中的一种容器类型, 可以同时存储多个元素.

Q2: 列表常用的函数有哪些?

  • 查: index(), count(), in, not in
  • 增: append(), extend(), insert()
  • 删: del, pop(), remove()
  • 改: 列表名[索引], reverse(), sort()

【掌握】元组的定义与使用

学习目标

  • 理解元组的定义格式及应用场景
  • 掌握元组的常用函数

为什么需要元组

思考:如果想要存储多个数据,但是这些数据是不能修改的数据,怎么做?

答:列表?列表可以一次性存储多个数据,但是列表中的数据允许更改。

1
2
num_list = [10, 20, 30]
num_list[0] = 100

那这种情况下,我们想要存储多个数据且数据不允许更改,应该怎么办呢?

答:使用==元组,元组可以存储多个数据且元组内的数据是不能修改的。==

元组的定义

元组特点:定义元组使用==小括号==,且使用==逗号==隔开各个数据,==数据可以是不同的数据类型。==

基本语法:

1
2
3
4
5
# 多个数据元组
tuple1 = (10, 20, 30)

# 单个数据元组
tuple2 = (10,)

注意:如果定义的元组只有一个数据,那么这个数据后面也要添加逗号,否则数据类型为唯一的这个数据的数据类型。

元组的相关操作方法

由于元组中的数据不允许直接修改,所以其操作方法大部分为查询方法。

编号 函数 作用
1 元组[索引] 根据==索引下标==查找元素
2 index() 查找某个数据,如果数据存在返回对应的下标,否则报错,语法和列表、字符串的index方法相同
3 count() 统计某个数据在当前元组出现的次数
4 len() 统计元组中数据的个数

案例1:访问元组中的某个元素

1
2
nums = (10, 20, 30)
print(nums[2])

案例2:查找某个元素在元组中出现的位置,存在则返回索引下标,不存在则直接报错

1
2
nums = (10, 20, 30)
print(nums.index(20))

案例3:统计某个元素在元组中出现的次数

1
2
nums = (10, 20, 30, 50, 30)
print(nums.count(30))

案例4:len()方法主要就是求数据序列的长度,字符串、列表、元组

1
2
nums = (10, 20, 30, 50, 30)
print(len(nums))
1
2
nums = (10, 20, 30, 50, 30)
print(len(nums))

元组案例

案例需求

编写一个程序来提取嵌套元组中的唯一元素。
例如: 在嵌套元组((1,2,3),(2,4,6),(2,3,5))中, 2重复出现了3次,3重复出现了2次,但我们的输出列表只会包含2、3一次。
即:[1, 2, 3, 4, 5, 6]

实现思路

①定义一个嵌套元组

②使用循环方式遍历元组中的每个子元组

③嵌套循环遍历子元组中的每个元素

④将元素添加到集合中,集合天然去重

⑤将集合转化为列表

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 定义一个嵌套元组
nested_tuple = ((1, 2, 3), (2, 4, 6), (2, 3, 5))

# 2. 初始化一个空集合用于存储唯一元素
unique_elements = set()

# 3. 使用 for 循环遍历嵌套元组中的每个子元组
for sub_tuple in nested_tuple:
# 4. 使用 for 循环遍历子元组中的每个元素
for element in sub_tuple:
# 5. 将元素添加到集合中(集合会自动去重)
unique_elements.add(element)

# 6. 将集合转换为列表
unique_list = list(unique_elements)

# 7. 打印唯一元素列表
print(f"嵌套元组中的唯一元素是: {unique_list}")

巩固练习

给定一个元组my_tuple,里面包含1, 2, 3, 4, 5, 6, 7, 8, 9元素,要求统计数字元组中, 奇数的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.定义一个数字元组
my_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9)

# 2.初始化奇数计数器
odd_count = 0

# 3.遍历元组中的每个元素
for num in my_tuple:
# 4.检查元素是否为奇数
if num % 2 != 0:
odd_count += 1

# 5.输出奇数的个数
print("元组中奇数的个数:", odd_count)

总结

Q1: 元组的特点

  • 可以存储多个元素, 但是元素值不可变.

Q2: 元组的常用函数

  • index(), count(), len()

【掌握】字典的定义与使用

学习目标

  • 掌握字典定义格式及相关函数
  • 掌握字典在实际开发中的应用场景

为什么需要字典(dict)

思考1:比如我们要存储一个人的信息,姓名:Tom,年龄:20周岁,性别:男,家庭住址:北京市昌平区,如何快速存储。

1
person = ['Tom', 20, '男', '北京市昌平区']

思考2:在日常生活中,姓名、年龄以及性别同属于一个人的基本特征。但是如果使用列表对其进行存储,则分散为3个元素,这显然不合逻辑。我们有没有办法,将其保存在同一个元素中,姓名、年龄以及性别都作为这个元素的3个属性。

答:使用Python中的字典

Python中字典(dict)的概念

特点:

① 符号为==大括号==(花括号) => {}

② 数据为==键值对==形式出现 => {key:value},key:键名,value:值,在同一个字典中,key必须是唯一(类似于索引下标)

③ 各个键值对之间用==逗号==隔开

image-20210602110608215

在字典中,键名除了可以使用字符串的形式,还可以使用数值的形式来进行表示

定义:

1
2
3
4
5
6
7
# 有数据字典
dict1 = {'name': 'Tom', 'age': 20, 'gender': '男'}

# 空字典
dict2 = {}

dict3 = dict()

在Python代码中,字典中的key必须使用引号引起来

字典的增操作(重点)

基本语法:

1
2
字典名称[key] = value
注:如果key存在则修改这个key对应的值;如果key不存在则新增此键值对。

案例:定义一个空字典,然后添加name、age以及address这样的3个key

1
2
3
4
5
6
7
8
# 1、定义一个空字典
person = {}
# 2、向字典中添加数据
person['name'] = '刘备'
person['age'] = 40
person['address'] = '蜀中'
# 3、使用print方法打印person字典
print(person)

注意:列表、字典为可变类型

字典的删操作

① del 字典名称[key]:删除指定元素

1
2
3
4
5
6
# 1、定义一个有数据的字典
person = {'name':'王大锤', 'age':28, 'gender':'male', 'address':'北京市海淀区'}
# 2、删除字典中的某个元素(如gender)
del person['gender']
# 3、打印字典
print(person)

② clear()方法:清空字典中的所有key

1
2
3
4
5
6
# 1、定义一个有数据的字典
person = {'name':'王大锤', 'age':28, 'gender':'male', 'address':'北京市海淀区'}
# 2、使用clear()方法清空字典
person.clear()
# 3、打印字典
print(person)

字典的改操作

基本语法:

1
2
字典名称[key] = value
注:如果key存在则修改这个key对应的值;如果key不存在则新增此键值对。

案例:定义一个字典,里面有name、age以及address,修改address这个key的value值

1
2
3
4
5
6
# 1、定义字典
person = {'name':'孙悟空', 'age': 600, 'address':'花果山'}
# 2、修改字典中的数据(address)
person['address'] = '东土大唐'
# 3、打印字典
print(person)

字典的查操作

① 查询方法:使用具体的某个key查询数据,如果未找到,则直接报错。

1
字典序列[key]

② 字典的相关查询方法

编号 函数 作用
1 keys() 以列表返回一个字典所有的键
2 values() 以列表返回字典中的所有值
3 items() 以列表返回可遍历的(键, 值) 元组数据

案例1:提取person字典中的所有key

1
2
3
4
# 1、定义一个字典
person = {'name':'貂蝉', 'age':18, 'mobile':'13765022249'}
# 2、提取字典中的name、age以及mobile属性
print(person.keys())

案例2:提取person字典中的所有value值

1
2
3
4
# 1、定义一个字典
person = {'name':'貂蝉', 'age':18, 'mobile':'13765022249'}
# 2、提取字典中的貂蝉、18以及13765022249号码
print(person.values())

案例3:使用items()方法提取数据

1
2
3
4
5
6
7
# 1、定义一个字典
person = {'name':'貂蝉', 'age':18, 'mobile':'13765022249'}
# 2、调用items方法获取数据,dict_items([('name', '貂蝉'), ('age', 18), ('mobile', '13765022249')])
# print(person.items())
# 3、结合for循环对字典中的数据进行遍历
for key, value in person.items():
print(f'{key}{value}')

字典案例

案例需求

给定一个字符串my_string,现在要求统计每个字符出现的次数

实现思路

①定义一个字符串

②初始化空字典,来存储对应字符和出现次数

③循环遍历字符串中每个字符

④如果字符串已经在字典中,计数加1,如果不在,初始化计数1

⑤输出统计每个字符出现的次数

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1.定义一个字符串
my_string = "hello world"

# 2.初始化一个空字典来存储字符及其出现的次数
char_count = {}

# 3.遍历字符串中的每个字符
for char in my_string:
# 4.如果字符已经在字典中,计数加一
if char in char_count:
char_count[char] += 1
# 5.如果字符不在字典中,初始化计数为1
else:
char_count[char] = 1

# 6.输出每个字符出现的次数
for char, count in char_count.items():
print(f"字符 '{char}' 出现了 {count} 次")

巩固练习

需求: 编写一个程序将字符串转换为字典例如:输入: ‘5=Five 6=Six 7=Seven’ 输出: {‘5’: ‘Five’, ‘6’: ‘Six’, ‘7’: ‘Seven’}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 从输入获取一个字符串
input_string = input("请输入一个字符串: ")

# 2. 初始化一个空字典用于存储转换后的键值对
result_dict = {}

# 3. 使用 split() 方法将字符串按空格分割成多个键值对字符串
key_value_pairs = input_string.split()

# 4. 使用 for 循环遍历每个键值对字符串
for pair in key_value_pairs:
# 5. 使用 split('=') 方法将键值对字符串按 '=' 分割成键和值
key, value = pair.split('=')

# 6. 将键和值添加到字典中
result_dict[key] = value

# 7. 打印转换后的字典
print(f"转换后的字典是: {result_dict}")

总结

Q1: 字典的特点

  • 存储的是键值对的元素
  • 键具有唯一性, 值可以重复

Q2:字典的常用函数

  • clear(), keys(), values(), items()

【熟悉】集合的定义与使用

学习目标

  • 理解集合的定义格式及特点

什么是集合

集合(set)是一个无序的不重复元素序列。

① 天生去重

② 无序

集合的定义

在Python中,我们可以使用一对花括号{}或者set()方法来定义集合,但是如果你定义的集合是一个空集合,则只能使用set()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 定义一个集合
s1 = {10, 20, 30, 40, 50}
print(s1)
print(type(s1))

# 定义一个集合:集合中存在相同的数据
s2 = {'刘备', '曹操', '孙权', '曹操'}
print(s2)
print(type(s1))

# 定义空集合
s3 = {}
s4 = set()
print(type(s3)) # <class 'dict'>
print(type(s4)) # <class 'set'>

集合操作的相关方法(增删查)

☆ 集合的增操作

add()方法:向集合中增加一个元素(单一)

1
2
3
4
students = set()
students.add('李哲')
students.add('刘毅')
print(students)

☆ 集合的删操作

remove()方法:删除集合中的指定数据,如果数据不存在则报错。

1
2
3
4
5
# 1、定义一个集合
products = {'萝卜', '白菜', '水蜜桃', '奥利奥', '西红柿', '凤梨'}
# 2、使用remove方法删除白菜这个元素
products.remove('白菜')
print(products)

☆ 集合中的查操作

① in :判断某个元素是否在集合中,如果在,则返回True,否则返回False

② not in :判断某个元素不在集合中,如果不在,则返回True,否则返回False

1
2
3
4
5
6
7
# 定义一个set集合
s1 = {'刘帅', '英标', '高源'}
# 判断刘帅是否在s1集合中
if '刘帅' in s1:
print('刘帅在s1集合中')
else:
print('刘帅没有出现在s1集合中')

③ 集合的遍历操作

1
2
for i in 集合:
print(i)

集合案例

需求

编写一个程序来统计缺失的数字并返回它们的总和。缺失的数字是指给定列表中两个极端(最大和最小数字)之间没有出现的数字。
例如,在列表[2, 5, 3, 7, 5, 7]中,两个极端(即2和7)之间缺失的数字是4和6。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1. 从输入获取一个整数列表
numbers = [2, 5, 3, 7, 5, 7]

# 2. 找到列表中的最小值
min_num = min(numbers)

# 3. 找到列表中的最大值
max_num = max(numbers)

# 4. 初始化一个集合用于存储列表中的所有数字
number_set = set(numbers)

# 5. 初始化一个变量用于存储缺失数字的总和
missing_sum = 0

# 6. 遍历从最小值到最大值之间的每个数字
for num in range(min_num + 1, max_num):
# 7. 检查数字是否不在集合中
if num not in number_set:
# 8. 如果不在,将数字添加到缺失数字的总和中
missing_sum += num

# 9. 打印缺失数字的总和
print(f"缺失的数字之和是: {missing_sum}")

总结

Q1: 集合的特点

  • 无序, 唯一

Q2: 集合的应用场景

  • 适用于 元素的去重 情况

作业

作业一:集合中奇数和

给定一个集合numbers,集合中包含1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15元素,求该集合中所有奇数的和是多少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. 定义一个集合记录一些整数
numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}

# 2. 初始化一个变量用于存储奇数的和
odd_sum = 0

# 3. 遍历集合中的每个数字
for num in numbers:
# 4. 检查数字是否为奇数
if num % 2 != 0:
# 5. 如果是奇数,将其加到奇数的和中
odd_sum += num

# 6. 打印所有奇数的和
print(f"集合中所有奇数的和是: {odd_sum}")

面向对象基础

面向对象和面向过程

  • 编程思想

    就是人们利用计算机来解决问题的思维.

  • 分类

    • 面向过程

      它是一种编程思想, 强调的是以 步骤(过程) 为基础完成各种操作.

    • 面向对象

      参考思路: 概述, 思想特点, 举例, 总结

      它是一种编程思想, 强调的是以 对象 为基础完成各种操作, 它是基于 面向过程的.
      说到面向对象, 不得不提的就是它的三大思想特点:

       1. 更符合人们的思考习惯.
       2.  把复杂的事情简单化.
       3. 把人们(程序员)从执行者变成指挥者.
      

      举例: 越符合当时的场景越好, 例如: 买电脑, 洗衣服…

      总结: 万物接对象.

面向对象特征介绍

  • 三大特征

    • 封装
    • 继承
    • 多态
  • 封装简介

    • 概述

      就是隐藏对象的属性和实现细节, 仅对外提供公共的访问方式.

    • 举例

      • 电脑, 手机, 函数, 类 = 属性 + 行为
    • 好处

      提高代码的安全性. (私有化)

      提高代码的复用性. (函数)

  • 继承

    • 概述

      子类继承父类的成员, 例如: 属性, 行为等.
      大白话: 子承父业.

    • 好处

      提高代码的复用性.

  • 多态

    • 概述

      大白话: 同一个事物在不同时刻表现出来的不同状态, 形态.

      专业版: 同1个函数, 接收不同的对象, 有不同的效果。

入门案例_汽车类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
"""
1:面向对象概念:
类:抽象的概念 ,看不见 、摸不着
类=属性(生活:名词 编程:变量)+方法(生活:动词(行为) 编程:函数)
对象:类的具体体现 类的实现(实例化一个类)
属性:用来描述事物的特征(高矮胖瘦....)
和之前定义变量一样
行为:用来描述事物能干什么 吃、喝、睡
和之前定义函数一样
2:类的格式
class 类名:
属性+方法
3:对象: 通过汽车图纸生产出来其他(即对象)
3-1:对象语法格式
定义:对象名 = 类()
调用: 对象名.方法名()
"""


class Car:
# 当前无属性
# 当前有方法 函数 def 函数名(有参、无参):
def run(self):
print("我跑起来了")


# 1:创建一个汽车类对象
# 定义:对象名 = 类()
c1 = Car()

# 2:调用对象调用
# 对象名.方法名()
c1.run()


self关键字介绍

  • 案例1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    """
    self关键字:
    概述:
    代表本类当前【对象】的引用,谁(哪个对象)调用,self就代表谁
    作用:
    用于实现函数【区分不同对象】
    总结:大白话
    1:如果你是C1对象,那么self值,代表C1,如果C2调用的话,self值就代表C2
    2:函数内部都一个self,哪个对象调用,我的self就代表谁
    同学问问题:
    张三的老师在给张三解决问题 李四的老师在给李四解决问题
    总结: 在类内 和 类外方法的调用
    1:在类内, 访问类中的行为(函数),通过self.的方式进行访问
    2:在类外 ,访问类中的行为(函数),通过对象名.函数进行访问
    """


    # 1、类的定义
    class Car :
    # 属性 和行为
    def run(self):
    print(f"{self}跑起来了")

    def work(self):
    print(f"我是work函数,我的self值是:",{self})
    #我想在work里面调用run
    # run() 在类内访问类中的函数(行为)必须使用self.方式
    self.run() # = 本类当前对象的引用


    # 2、类的调用
    c1 = Car()
    print(f"C1对象:", {c1}) # 0x000001BC70B8B640
    c1.run() # 0x000001BC70B8B640
    print("*"*10)
    c1.work()

    # 3.定义一个新的对象
    print("-" * 24)
    c2 = Car()
    print(f"C2对象:", {c2}) # 0x000002B8B275B250
    c2.run() # 0x000002B8B275B250

    # 4.小结:c1和c2打印的值不同 ,因为他们是不同的对象。self代表的c1 和c2
    # 当c1调用的时候self就代表c1 当c2调用的时候self就代表c2

入门案例_手机类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
"""
回顾:
类定义格式
class 类名:
属性(变量)
方法(函数)
访问: 类中行为访问
类内:self.
类外:对象名.
"""


class Phone:

# 行为=方法=函数
def open(self):
print(f"{self}手机开机了")


def close(self):
print(f"{self}手机关机了")

def take_photo(self):
print(f"{self}手机可以拍照了")

# 2、创建手机类的对象 = 实例化手机类对象 访问其成员(方法、属性)
#以为都是p1 ,因为对象都是一个
p1=Phone()
print(f"p1对象:",{p1})
p1.open()
p1.take_photo()
p1.close()
print("*"*24)

#3、继续创建手机对象
p2=Phone()
print(f"p2对象:",{p2})
p2.open()
p2.take_photo()
p2.close()
print("*"*24)

类外_获取和设置对象的属性

1742590962905

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
"""
属性设置:
【属性赋值】对象名.属性名=属性值
【属性调用】 对象名.属性名

类外 设置属性
对象名.属性名=属性值
特点:该属性独属于这个对象。即该类的其他对象没有这个属性

类内 设置属性
self.属性名=属性值
"""


class Car:
def run(self):
print("汽车会跑")

# 类外 创建该类的对象名 -》此位置 类外的位置
c1 = Car()
c1.run()
# 细节1:对C1设置属性值
c1.color = '红色'
c1.number = 4

# 细节2:打印C1的对象属性值
print(f"颜色:{c1.color},轮胎数:{c1.number}")

print("*" * 24)
# 定义其他对象
c2 = Car()
c2.run()
# c2调用属性
print(c2.color)
print(c2.number)
# 分析:报错, c2尝试去color和number,因为c1设置是类外属性,仅仅对c1有效。c2找不到


类内_获取对象的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"""
例如,在类内部定义一个show()方法来获取刚刚给车设置颜色为红色、4个轮胎的属性值信息。
"""


# 1:创建汽车类
class Car:

def run(self):
print("汽车会跑")

def show(self):
print(f"我是show函数,对象颜色是{self.color},对象轮胎数:{self.number}")


# 2:根据汽车类创建对象
c1 = Car()

# 3:给C1进行赋值 类外赋值
c1.color = "红色"
c1.number = 4

# 4:类外访问属性
print(f"颜色:{c1.color},轮胎数{c1.number}")

# 5:类外访问行为(类中的函数)
c1.run()
c1.show()

# 6.继续创建汽车类的对象,尝试调用run和show函数
c2 = Car()
c2.run()
c2.show() # 解释通过(car类内中有show方法),运行报错(show方法里面调用的属性,c2没有定义)

# 总结
# 1:【类外】访问类中的成员,无论是设置还是获取,都是通过 对象名.方法
# 2:【类内】访问类中的成员,无论是设置还是获取,都是通过 self.方法
# 3:如果类外设置属性,那么属性仅仅是当前的对象可以使用。如果想让所有的对象都可以使用,设置在类内?
# 如何设置 ? 采用魔法方法__init__

魔法方法之init方法

  • 图解

    1742591021157

  • 案例1: 无参数版

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    """
    魔法方法:
    Python内置函数,在满足特点的场景下,会被自动调用
    场景魔法方法:
    __init__ 在(每次)创建对象的时候,会自动触发该类__init__()函数
    __str__
    __del__
    """


    # 给车这个对象默认设置color(颜色)和number(轮胎数)为黑色、3个轮胎。
    #1.定义类
    class Car:
    # 函数 、方法、 行为
    def __init__(self):
    print("我是无参init魔法方法")
    self.color='黑色'
    self.number=3
    def show(self):
    print(f"颜色{self.color} , 轮胎数{self.number}")

    # 演示:就是每创建一次对象,调用一次init方法
    # 2.创建汽车类对象
    c1 = Car()
    # c1.__init__()
    c1.show()
    print("*" * 24)
    c2 = Car()
    #结论:在(每次)创建对象的时候,会自动触发该类__init__()函数

    #演示初始化车参数 (颜色 轮胎)
    # 调用
    print(c1.color,c1.number)
    #修改
    c2.show()
    print("*"*24)
    c2.color='蓝色'
    c2.number=4
    print(c2.color,c2.number)
    c2.show()


  • 案例2: 有参数版

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    """
    回顾:
    __init__()魔法方法,在创建对象的时候,会被自动调用。一般用于给该类的属性做初始化
    初始化:
    有参数初始化:
    """
    # 1、创建汽车类,并且初始化带参的方法。
    class Car:
    # 初始化带参数
    def __init__(self, color, number):
    self.color = color
    self.number = number

    def show(self):
    print(f"颜色{self.color} 轮胎数:{self.number}")

    # 2、实例化Car类的对象
    c1 = Car() # __init__() missing 2 required positional arguments: 'color' and 'number'
    # c1 = Car("黄色",4)
    # 当带参数初始化方法出现的时候,即 在创建对象的时候。可以给对象予以初始化值,否则执行报错
    # c1.show()

魔法方法之str方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"""
魔法方法:
Python内置函数,在满足特点的场景下,会被自动调用
场景魔法方法:
__init__ 在(每次)创建对象的时候,会自动触发该类__init__()函数
__str__ 当print()函数打印对象的时候,会自动调用该类(所在类)的str魔法方法
当前方法默认打印的是对象的内存地址值,无意义,一般都会对方法进行重写改写。
改写为打印各个属性值
__del__
"""

# 例如,在输出car对象时,把它的颜色color和轮胎数number属性值显示出来。
#show 方法
class Car:
def __init__(self,color,number):
self.color=color
self.number=number

#如果没有讲str方法,我现在想打印属性值,该如何操作?
def show(self):
print(f"颜色{self.color} 轮胎数{self.number}")
def __str__(self):
return f"str重新改写后的显示格式:颜色{self.color} 轮胎数{self.number}"
#2.实例化对象
c1=Car("紫色",4)
# c1.show()
# print(c1) #<__main__.Car object at 0x000001386961B640>
#这里其实走了__str__ 这个方法,因为我们没有对这个方法进行重写改写,所以这里打印的是地址值
#3.改写str魔法方法
print(c1) #此时已经重新写了str方法

魔法方法之del方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"""
魔法方法:
Python内置函数,在满足特点的场景下,会被自动调用
场景魔法方法:
__init__ 在(每次)创建对象的时候,会自动触发该类__init__()函数
__str__ 当print()函数打印对象的时候,会自动调用该类(所在类)的str魔法方法
当前方法默认打印的是对象的内存地址值,无意义,一般都会对方法进行重写改写。
改写为打印各个属性值
__del__ 当.py文件执行结果后,或者手动del 释放对象资源的时候,会自动调用该函数
"""
# 例如,定义一个有品牌属性的汽车类,并使用__del__()方法删除对象查看效果。
class Car:
# 2、在魔法方法init中完成属性初始化
def __init__(self, brand):
self.brand = brand

# 3、重写改写str魔法方法 打印对象属性值
def __str__(self):
return f"品牌:{self.brand}"

# 4、重写改写魔法删除方法 ,删除对象的时候给出提示
def __del__(self):
print(f"{self}对象被删除了")
# 5、创建汽车类对象
c1 = Car("小米")
# print(c1) #验证:str #程序执行结束后自动删除

#6、演示手动删除
# del c1
print(c1)
print("程序结束")

减肥案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 例如小明同学当前体重是100kg。每当他跑步一次时,
# # 则会减少0.5kg;每当他大吃大喝一次时,则会增加2kg。
# 请试着采用面向对象方式完成案例。
"""
类: 学生类
对象:小明
属性 : 体重
行为:跑步 、大吃大喝
"""


# 1.类 同学
class Student: # 大驼峰
# 类=属性+方法(行为)
# 2.当前体重是100kg
def __init__(self,name):
self.name =name
self.current_weigth = 100

# 3、行为:跑步 、大吃大喝
def run(self):
print("疯狂跑步....")
self.current_weigth -= 0.5

def eat(self):
print("大吃一顿")
self.current_weigth += 2

def __str__(self):
return f"{self.name}当前体重{self.current_weigth}"
# 4、实例化学生对象
xm = Student("小明")
xm.run()
xm.eat()
print(xm)

烤地瓜案例

1742591192478

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
"""
类:地瓜类
属性:烤的时间、烤的状态、调料
行为:烤、添加调料
"""


# SweetPotato 地瓜
class SweetPotato:
# 类=属性+行为
# 初始化地瓜都是生的 属性
def __init__(self):
self.cook_time = 0
self.cook_state = '生的'
self.seasoning = [] # 添加调料

# 烤
def cook(self, time):
if time < 0:
print("无效值!")
else:
self.cook_time += time # 假设time传递的时间为2,即这个语句 0+2,相当于2分钟,如果再传5分钟 2+5 =7 熟了
if 0 <= self.cook_time < 3:
self.cook_state = "生的"
elif 3 <= self.cook_time < 7:
self.cook_state = "半生不熟"
elif 7 <= self.cook_time < 12:
self.cook_state = "熟了"
else:
self.cook_state = "糊了!!!"

# 添加调料
def addSeasoning(self, seasoning):
# self.seasoning = seasoning
self.seasoning.append(seasoning)

def __str__(self): # 格式化代码 ctrl+a 全选 ctrl+alt+l
return f"烤的时间{self.cook_time} ,地瓜状态{self.cook_state},调料{self.seasoning}"


# 实例化对象
sp = SweetPotato()
#初始化地瓜状态
print(sp.cook_time)
print(sp.cook_state)
print(sp.seasoning)

#
sp.cook(2)
print(sp)
sp.cook(5)
print(sp)
sp.addSeasoning("玛莎拉")
sp.addSeasoning("胡椒")
sp.addSeasoning("辣椒")
print(sp)

创建类的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""
1:在python中,创建类有如下几种方式

格式1:
class 类名:
格式2:
class 类名():
格式3:
class 类名(object)
"""


# 需求 定义老师类
class Teacher(object): # object是所有类的父类,python中所有的类都直接或者间接的继承object类
pass

继承入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""
在python中继承格式:
class 父类名称(object):
pass
class 子类名称(父类名称):
pass
提示:object是所有类的父类 object类顶级类或者基类 ,其他的子类派生类

好处:提高了代码复用性
弊端:耦合性,父类有不好的内容,子类想没有都不行 高内聚,低耦合
大白话:自己能搞定的事情,不要麻烦别人


例如,Father类有一个默认性别为男,且爱好散步行走,那么,Son类也想要拥有这些属性和行为,该怎么做呢?
"""


# 1、定义父类
class Father(object):
def __init__(self):
self.gender = "男"

def walk(self):
print("饭后走一走,活到九十九")
# 2、定义子类
class Son(Father):
pass
# 3、测试儿子类
s1 = Son()
print(s1.gender) # 打印男 ,继承自父类
s1.walk() # 饭后走一走,活到九十九 继承自父类

单继承演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 一个摊煎饼的老师傅,在煎饼果子界摸爬滚打多年,
# 研发了一套精湛的摊煎饼技术,
# 师父要把这套技术传授给他的唯一的最得意的徒弟。

# 老师傅
class Master:
def __init__(self):
self.kongfu = '[古法配方]'

def make_cake(self):
print(f"采用{self.kongfu}摊煎饼果子")
# Apprentice 徒弟
class Apprentice(Master):
pass

# 测试 实例化徒弟类
app = Apprentice()
app.make_cake()

深度学习简介

什么是深度学习[知道]

深度学习(Deep Learning)是机器学习的分支,是一种以人工神经网络为架构对数据进行特征学习的算法。深度学习中的形容词“深度”是指在网络中使用多层。

1755683237776

深度学习核心思想是==通过模仿人脑的神经网络来处理和分析复杂的数据,从大量数据中自动提取复杂特征==,擅长处理高维数据,如图像、语音和文本。

1755683268024

深度学习特点[知道]

  • 多层非线性变换:深度学习模型由多个层次组成,每一层都应用非线性激活函数对输入数据进行变换。较低的层级通常捕捉到简单的特征(如边缘、颜色等),而更高的层级则可以识别更复杂的模式(如物体或面部识别)。

  • 自动特征提取:与传统机器学习算法不同,深度学习能够自动从原始数据中学习到有用的特征,而不需要人工特征工程。这使得深度学习在许多领域中表现出色。

  • 大数据和计算能力:深度学习模型通常需要大量的标注数据和强大的计算资源(如GPU)来进行训练。大数据和高性能计算使得深度学习在图像识别、自然语言处理等领域取得了显著突破。

  • 可解释性差:深度学习模型内部的运作机制相对不透明,被称为“黑箱”,这意味着理解模型为什么做出特定决策可能会比较困难。这对某些应用场景来说是一个挑战。

常见的深度学习模型[了解]

  • 卷积神经网络 (Convolutional Neural Networks, CNN)
    • 主要用于图像处理任务,如图像分类、目标检测、图像分割等。
    • 特点是使用卷积层来自动提取图像中的局部特征,并通过池化层减少参数数量,提高计算效率。
  • 循环神经网络 (Recurrent Neural Networks, RNN)
    • 适用于处理序列数据,例如自然语言处理(NLP)、语音识别等。
    • RNN具有记忆功能,可以处理输入数据的时间依赖性,但标准RNN难以捕捉长期依赖关系。
  • 自编码器 (Autoencoders)
    • 一种无监督学习模型,通常用于降维、特征学习或者异常检测。
    • 自编码器由编码器和解码器两部分组成,前者将输入压缩成一个较低维度的表示,后者尝试从这个低维表示重建原始输入。
  • 生成对抗网络 (Generative Adversarial Networks, GAN)
    • 包含两个子网络:生成器和判别器。生成器负责创建看起来真实的假样本,而判别器则试图区分真假样本。
    • GAN广泛应用于图像生成、视频合成等领域。
  • Transformer
    • 主要用于自然语言处理(NLP)任务,尤其是机器翻译、文本生成等。
    • Transformer摒弃了传统的递归结构,采用自注意力机制(self-attention),使得它能够并行处理整个句子的信息,在机器翻译、文本摘要等任务中表现出色。

深度学习应用场景[了解]

  • ==计算机视觉(Computer Vision)==

    • 图像分类:将图像分为不同的类别。常用于人脸识别、物体检测等。

      • 自动标注社交媒体照片、医疗影像中的病变检测。
    • 目标检测(Object Detection):在图像或视频中定位并分类多个对象。

      • 自动驾驶中的行人检测、监控视频中的入侵检测。
    • 面部识别:通过分析面部特征进行身份验证或分类。

      • 手机解锁、安防监控系统。
    • 图像生成:基于输入生成新的图像,如风格转换、图像超分辨率等。

      • 艺术风格迁移、老旧照片修复。
  • ==自然语言处理(Natural Language Processing, NLP)==

    • 机器翻译:使用深度学习模型将一种语言的文本自动翻译成另一种语言。

      • Google翻译、实时语音翻译。
    • 情感分析:分析文本中的情感倾向,如正面、负面或中性。

      • 社交媒体监控、产品评论分析。
    • 文本生成:生成符合语法和语义的自然语言文本。

      • 自动写作助手、新闻生成。
    • 语音识别:将语音转化为文字。

      • 智能助手(如Siri、Alexa)、自动字幕生成。
    • 聊天机器人(Chatbot):通过深度学习理解用户输入并生成合理的回应。

      • 客服机器人、虚拟助手(如GPT类模型)。
  • ==推荐系统(Recommendation Systems)==

    • 电影、音乐推荐:根据用户历史的评分和行为,推荐相关的电影、音乐或电视剧。
      • Netflix、Spotify的个性化推荐。
    • 电商推荐:根据用户的购买历史和浏览习惯推荐商品。
      • 亚马逊、淘宝的商品推荐系统。
    • 社交媒体推荐:分析用户的社交行为,推荐相关内容或朋友。
      • Facebook、Instagram的内容推荐。

1755683296538

深度学习发展史[了解]

  • 早期探索

    • 20世纪40年代:沃尔特·皮茨(Walter Pitts)和沃伦·麦卡洛克(Warren McCulloch)等开始模仿生物神经系统来构建计算模型,如McCulloch-Pitts神经元
    • 1958年:弗兰克·罗森布拉特(Frank Rosenblatt)提出感知器概念,能够进行简单的二分类任务
    • 1960年代末:出现了多层感知器(MLP),但当时由于计算能力和数据量的限制,这些模型的应用受到很大限制
  • 挑战与瓶颈

    • 1986年:反向传播算法(Backpropagation)的提出标志着神经网络研究的一个重要突破。杰弗里·辛顿(Geoffrey Hinton)和大卫·鲁梅尔哈特(David Rumelhart)等人提出了反向传播算法,使得多层神经网络(即深层网络)能够通过梯度下降优化参数,解决复杂的非线性问题。
    • 虽然神经网络方法在一些领域表现不错,但由于计算资源的限制以及对复杂数据(如图像和语音)的处理能力较弱,深度学习未能广泛应用。此时,支持向量机(SVM)决策树等传统机器学习方法成为主流。
  • 复兴与突破

    • 2006年杰弗里·辛顿和其团队提出了深度信念网络(DBN),标志着深度学习的复兴。他们引入了无监督预训练的技术,使得深层网络能够有效训练。这为深度学习的发展奠定了基础。
    • 2012年:深度学习的一个重要突破是AlexNet的出现。亚历克斯·克里泽夫斯基(Alex Krizhevsky)在ImageNet图像分类竞赛中使用了一个深度卷积神经网络,显著提升了图像分类的精度,比传统方法提高了20%以上。AlexNet的成功标志着深度学习在计算机视觉领域的成功应用。
    • 2014年:生成对抗网络(GANs)由伊恩·古德费洛(Ian Goodfellow)等人提出,开启了生成模型的新时代,能够生成非常逼真的图像、音频和视频。
    • 2015年ResNet(残差网络)由何凯明(Kaiming He)等提出,解决了深度网络中的梯度消失和梯度爆炸问题,允许训练极深的网络(如50层、152层),极大推动了深度学习在图像识别任务中的应用。
  • 爆发期

    • **2016年:**Google AlphaGo 战胜李世石(人工智能第三次浪潮),AlphaGo 展现了深度强化学习(Deep Reinforcement Learning)在解决复杂问题上的巨大潜力,将其推向了公众视野。
    • **2017年:**自然语言处理NLP的Transformer框架出现,奠定了后续预训练语言模型(如 BERT 和 GPT)的基础。
    • **2018年:**BERT和GPT的出现,基于Transformer架构的预训练语言模型的代表。
    • **2022年:**ChatGPT的出现,进入到大模型AIGC发展的阶段,开启了 AI 与人交互的新模式,使人们可以更容易地使用 AI 并从中受益。

1755683316439